diff --git a/radicle-cli/examples/git/git-pull.md b/radicle-cli/examples/git/git-pull.md index 31b434c8..652254b7 100644 --- a/radicle-cli/examples/git/git-pull.md +++ b/radicle-cli/examples/git/git-pull.md @@ -1,30 +1,30 @@ ``` $ cd heartwood $ git branch -r + alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master rad/master - z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master ``` ``` -$ git ls-remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*' +$ git ls-remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*' 145e1e69bef3ad93d14946ea212249c2fa9b9828 refs/heads/alice/1 f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master ``` ``` (stderr) -$ git fetch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi +$ git fetch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi - * [new branch] alice/1 -> z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1 + * [new branch] alice/1 -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1 ``` ``` $ git branch -r + alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1 + alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master rad/master - z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1 - z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master ``` ``` -$ git rev-parse z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1 +$ git rev-parse alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1 145e1e69bef3ad93d14946ea212249c2fa9b9828 ``` diff --git a/radicle-cli/examples/rad-auth.md b/radicle-cli/examples/rad-auth.md index 343adc4d..5452266f 100644 --- a/radicle-cli/examples/rad-auth.md +++ b/radicle-cli/examples/rad-auth.md @@ -2,7 +2,7 @@ Initializing a new identity with `rad-auth`. The example below is run with `RAD_PASSPHRASE` set. ``` -$ rad auth +$ rad auth --alias "alice" Initializing your radicle ๐Ÿ‘พ identity @@ -18,3 +18,9 @@ You can get the above information at all times using the `self` command: $ rad self --did did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ``` + +You can also show your alias: +``` +$ rad self --alias +alice +``` diff --git a/radicle-cli/examples/rad-clone-all.md b/radicle-cli/examples/rad-clone-all.md index 9862278d..5b5b5337 100644 --- a/radicle-cli/examples/rad-clone-all.md +++ b/radicle-cli/examples/rad-clone-all.md @@ -7,8 +7,8 @@ $ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope all โœ“ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSLโ€ฆStBU8Vi.. โœ“ Forking under z6Mkux1โ€ฆnVhib7Z.. โœ“ Creating checkout in ./heartwood.. -โœ“ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added -โœ“ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi +โœ“ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added +โœ“ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi โœ“ Repository successfully cloned under [..]/heartwood/ ``` @@ -68,10 +68,10 @@ And fetch his refs: ``` $ git fetch --all Fetching rad -Fetching z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi +Fetching alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi Fetching bob $ git branch --remotes + alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master bob/master rad/master - z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master ``` diff --git a/radicle-cli/examples/rad-clone.md b/radicle-cli/examples/rad-clone.md index 9149f130..8632875f 100644 --- a/radicle-cli/examples/rad-clone.md +++ b/radicle-cli/examples/rad-clone.md @@ -7,8 +7,8 @@ $ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted โœ“ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSLโ€ฆStBU8Vi.. โœ“ Forking under z6Mkt67โ€ฆv4N1tRk.. โœ“ Creating checkout in ./heartwood.. -โœ“ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added -โœ“ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi +โœ“ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added +โœ“ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLโ€ฆStBU8Vi โœ“ Repository successfully cloned under [..]/heartwood/ ``` @@ -27,8 +27,8 @@ Let's check that the remote tracking branch was setup correctly: ``` $ git branch --remotes + alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master rad/master - z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master ``` The first branch is ours, and the second points to the repository delegate. @@ -36,10 +36,10 @@ We can also take a look at the remotes: ``` $ git remote -v +alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch) +alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push) rad rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji (fetch) rad rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (push) -z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch) -z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push) ``` Let's check the last commit! diff --git a/radicle-cli/examples/rad-inspect-noauth.md b/radicle-cli/examples/rad-inspect-noauth.md index ab705687..2cde4aba 100644 --- a/radicle-cli/examples/rad-inspect-noauth.md +++ b/radicle-cli/examples/rad-inspect-noauth.md @@ -2,7 +2,7 @@ The `rad inspect` command can be run without being authenticated with radicle: ``` (fail) $ rad self -โœ— Self failed: Could not load radicle profile +โœ— Self failed: Could not load radicle profile: no profile found at path [..] โœ— Hint: To setup your radicle profile, run `rad auth`. ``` diff --git a/radicle-cli/examples/rad-merge-via-push.md b/radicle-cli/examples/rad-merge-via-push.md index 13da4b12..c60cfd40 100644 --- a/radicle-cli/examples/rad-merge-via-push.md +++ b/radicle-cli/examples/rad-merge-via-push.md @@ -67,12 +67,12 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE ``` ``` $ rad patch --merged -โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -โ”‚ โ— ID Title Author Head + - Updated โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ โœ” 0ec956c First change z6MknSLโ€ฆStBU8Vi (you) 20aa5dd +0 -0 [ ... ] โ”‚ -โ”‚ โœ” 928d76e Second change z6MknSLโ€ฆStBU8Vi (you) daf349f +0 -0 [ ... ] โ”‚ -โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ +โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โ— ID Title Author Head + - Updated โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โœ” 0ec956c First change z6MknSLโ€ฆStBU8Vi alice (you) 20aa5dd +0 -0 [ ... ] โ”‚ +โ”‚ โœ” 928d76e Second change z6MknSLโ€ฆStBU8Vi alice (you) daf349f +0 -0 [ ... ] โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` We can verify that the remote tracking branches were also deleted: diff --git a/radicle-cli/examples/rad-remote.md b/radicle-cli/examples/rad-remote.md index 3f5a4bd1..2dae29ba 100644 --- a/radicle-cli/examples/rad-remote.md +++ b/radicle-cli/examples/rad-remote.md @@ -48,6 +48,6 @@ able to fetch the node alias from our db! ``` $ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -โœ“ Remote bob added -โœ“ Remote-tracking branch bob/master created for z6Mkt67โ€ฆv4N1tRk +โœ“ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added +โœ“ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67โ€ฆv4N1tRk ``` diff --git a/radicle-cli/examples/rad-self.md b/radicle-cli/examples/rad-self.md index ad7257fa..a486efc8 100644 --- a/radicle-cli/examples/rad-self.md +++ b/radicle-cli/examples/rad-self.md @@ -3,12 +3,14 @@ device and node. ``` $ rad self +Alias alice DID did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi โ””โ•ดNode ID (NID) z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi SSH not running โ”œโ•ดKey (hash) SHA256:UIedaL6Cxm6OUErh9GQUzzglSk7VpQlVTI1TAFB/HWA โ””โ•ดKey (full) ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHahWSBEpuT1ESZbynOmBNkLBSnR32Ar4woZqSV2YNH1 Home [..]/home/alice/.radicle +โ”œโ•ดConfig [..]/home/alice/.radicle/config.json โ”œโ•ดStorage [..]/home/alice/.radicle/storage โ”œโ•ดKeys [..]/home/alice/.radicle/keys โ””โ•ดNode [..]/home/alice/.radicle/node diff --git a/radicle-cli/examples/workflow/5-patching-maintainer.md b/radicle-cli/examples/workflow/5-patching-maintainer.md index 73da84b2..9f88ba71 100644 --- a/radicle-cli/examples/workflow/5-patching-maintainer.md +++ b/radicle-cli/examples/workflow/5-patching-maintainer.md @@ -112,7 +112,7 @@ $ rad patch show 50e29a1 โ”‚ โ— opened by bob (z6Mkt67โ€ฆv4N1tRk) [ ... ] โ”‚ โ”‚ โ†‘ updated to 3530243d46a2e7a8e4eac7afcbb17cc7c56b3d29 (27857ec) [ ... ] โ”‚ โ”‚ โ†‘ updated to 744c1f0a75b1c42833c9aa32f79cd40443925d66 (f567f69) [ ... ] โ”‚ -โ”‚ โœ“ merged by (you) [ ... ] โ”‚ +โ”‚ โœ“ merged by alice (you) [ ... ] โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` diff --git a/radicle-cli/examples/workflow/6-pulling-contributor.md b/radicle-cli/examples/workflow/6-pulling-contributor.md index c707a1f4..486e2e20 100644 --- a/radicle-cli/examples/workflow/6-pulling-contributor.md +++ b/radicle-cli/examples/workflow/6-pulling-contributor.md @@ -24,7 +24,7 @@ $ git pull --all --ff From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji f2de534..f567f69 master -> rad/master From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi - f2de534..f567f69 master -> z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master + f2de534..f567f69 master -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master ``` Now our master branch is up to date with the maintainer's master: @@ -32,5 +32,5 @@ Now our master branch is up to date with the maintainer's master: ``` $ git rev-parse master f567f695d25b4e8fb63b5f5ad2a584529826e908 -$ git diff master..z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master +$ git diff master..alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master ``` diff --git a/radicle-cli/src/commands/auth.rs b/radicle-cli/src/commands/auth.rs index 35cae662..c9b71940 100644 --- a/radicle-cli/src/commands/auth.rs +++ b/radicle-cli/src/commands/auth.rs @@ -1,10 +1,13 @@ #![allow(clippy::or_fun_call)] +use std::env; use std::ffi::OsString; +use std::str::FromStr; use anyhow::anyhow; use radicle::crypto::ssh; use radicle::crypto::ssh::Passphrase; +use radicle::node::Alias; use radicle::profile::env::RAD_PASSPHRASE; use radicle::{profile, Profile}; @@ -26,6 +29,7 @@ Usage Options + --alias When initializing an identity, sets the node alias --stdin Read passphrase from stdin (default: false) --help Print help "#, @@ -34,6 +38,7 @@ Options #[derive(Debug)] pub struct Options { pub stdin: bool, + pub alias: Option, } impl Args for Options { @@ -41,10 +46,17 @@ impl Args for Options { use lexopt::prelude::*; let mut stdin = false; + let mut alias = None; let mut parser = lexopt::Parser::from_args(args); while let Some(arg) = parser.next()? { match arg { + Long("alias") => { + let val = parser.value()?; + let val = term::args::alias(&val)?; + + alias = Some(val); + } Long("stdin") => { stdin = true; } @@ -55,7 +67,7 @@ impl Args for Options { } } - Ok((Options { stdin }, vec![])) + Ok((Options { alias, stdin }, vec![])) } } @@ -81,6 +93,16 @@ pub fn init(options: Options) -> anyhow::Result<()> { anyhow::bail!("Error retrieving git version; please check your installation"); } + let alias: Alias = if let Some(alias) = options.alias { + alias + } else { + let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok()); + term::input( + "Enter your alias:", + user, + Some("This is your node alias. You can always change it later"), + )? + }; let home = profile::home()?; let passphrase = if options.stdin { term::passphrase_stdin() @@ -88,7 +110,7 @@ pub fn init(options: Options) -> anyhow::Result<()> { term::passphrase_confirm("Enter a passphrase:", RAD_PASSPHRASE) }?; let spinner = term::spinner("Creating your Ed25519 keypair..."); - let profile = Profile::init(home, passphrase.clone())?; + let profile = Profile::init(home, alias, passphrase.clone())?; spinner.finish(); match ssh::agent::Agent::connect() { diff --git a/radicle-cli/src/commands/checkout.rs b/radicle-cli/src/commands/checkout.rs index a31390cc..7a1c6993 100644 --- a/radicle-cli/src/commands/checkout.rs +++ b/radicle-cli/src/commands/checkout.rs @@ -160,14 +160,16 @@ pub fn setup_remote( remote_name: Option, aliases: &impl AliasStore, ) -> anyhow::Result<()> { - let remote_name = if let Some(alias) = remote_name { - alias + let remote_name = if let Some(name) = remote_name { + name } else { - let alias = aliases - .alias(remote_id) - .unwrap_or_else(|| remote_id.to_string()); - git::RefString::try_from(alias.clone()) - .map_err(|_| anyhow!("invalid remote name: '{alias}'"))? + let name = if let Some(alias) = aliases.alias(remote_id) { + format!("{alias}@{remote_id}") + } else { + remote_id.to_human() + }; + git::RefString::try_from(name.as_str()) + .map_err(|_| anyhow!("invalid remote name: '{name}'"))? }; let (remote, branch) = setup.run(remote_name, *remote_id)?; diff --git a/radicle-cli/src/commands/init.rs b/radicle-cli/src/commands/init.rs index a38d53b7..6c231948 100644 --- a/radicle-cli/src/commands/init.rs +++ b/radicle-cli/src/commands/init.rs @@ -198,14 +198,14 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> let name = options.name.unwrap_or_else(|| { let default = path.file_name().map(|f| f.to_string_lossy().to_string()); - term::input("Name", default).unwrap() + term::input("Name", default, None).unwrap() }); let description = options .description - .unwrap_or_else(|| term::input("Description", None).unwrap()); + .unwrap_or_else(|| term::input("Description", None, None).unwrap()); let branch = options.branch.unwrap_or_else(|| { if interactive.yes() { - term::input("Default branch", Some(head)).unwrap() + term::input("Default branch", Some(head), None).unwrap() } else { head } diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs index f507b360..89169b3f 100644 --- a/radicle-cli/src/commands/issue.rs +++ b/radicle-cli/src/commands/issue.rs @@ -462,6 +462,7 @@ fn list( ]); } table.print(); + Ok(()) } diff --git a/radicle-cli/src/commands/node/tracking.rs b/radicle-cli/src/commands/node/tracking.rs index d7e61265..05f32cd4 100644 --- a/radicle-cli/src/commands/node/tracking.rs +++ b/radicle-cli/src/commands/node/tracking.rs @@ -53,7 +53,7 @@ fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> { term::format::highlight(Did::from(id).to_string()), match alias { None => term::format::secondary(String::from("n/a")), - Some(alias) => term::format::secondary(alias), + Some(alias) => term::format::secondary(alias.to_string()), }, term::format::secondary(policy.to_string()), ]); diff --git a/radicle-cli/src/commands/patch/list.rs b/radicle-cli/src/commands/patch/list.rs index 60f0a959..e8bcb628 100644 --- a/radicle-cli/src/commands/patch/list.rs +++ b/radicle-cli/src/commands/patch/list.rs @@ -93,7 +93,7 @@ pub fn run( /// Patch row. pub fn row( profile: &Profile, - alias: Option, + alias: Option, id: &PatchId, patch: &Patch, repository: &Repository, diff --git a/radicle-cli/src/commands/self.rs b/radicle-cli/src/commands/self.rs index aae20aa9..568445dc 100644 --- a/radicle-cli/src/commands/self.rs +++ b/radicle-cli/src/commands/self.rs @@ -18,6 +18,7 @@ Usage Options + --alias Show your Node alias --nid Show your Node ID (NID) --did Show your DID --home Show your Radicle home @@ -29,6 +30,7 @@ Options #[derive(Debug)] enum Show { + Alias, NodeId, Did, Home, @@ -51,6 +53,9 @@ impl Args for Options { while let Some(arg) = parser.next()? { match arg { + Long("alias") if show.is_none() => { + show = Some(Show::Alias); + } Long("nid") if show.is_none() => { show = Some(Show::NodeId); } @@ -86,6 +91,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let profile = ctx.profile()?; match options.show { + Show::Alias => { + term::print(profile.config.alias()); + } Show::NodeId => { term::print(profile.id()); } @@ -110,6 +118,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { fn all(profile: &Profile) -> anyhow::Result<()> { let mut table = term::Table::default(); + table.push([ + term::format::style("Alias").to_string(), + term::format::primary(profile.config.alias()).to_string(), + ]); + let did = profile.did(); table.push([ term::format::style("DID").to_string(), @@ -153,6 +166,12 @@ fn all(profile: &Profile) -> anyhow::Result<()> { term::format::tertiary(home.display()).to_string(), ]); + let config_path = profile.home.config(); + table.push([ + term::format::style("โ”œโ•ดConfig").to_string(), + term::format::tertiary(config_path.display()).to_string(), + ]); + let storage_path = profile.home.storage(); table.push([ term::format::style("โ”œโ•ดStorage").to_string(), diff --git a/radicle-cli/src/commands/track.rs b/radicle-cli/src/commands/track.rs index 65d41aa1..08e56ded 100644 --- a/radicle-cli/src/commands/track.rs +++ b/radicle-cli/src/commands/track.rs @@ -79,10 +79,7 @@ impl Args for Options { } (Long("alias"), Some(Operation::TrackNode { alias, .. })) => { let name = parser.value()?; - let name = name - .to_str() - .to_owned() - .ok_or_else(|| anyhow!("alias specified is not UTF-8"))?; + let name = term::args::alias(&name)?; *alias = Some(name.to_owned()); } diff --git a/radicle-cli/src/terminal.rs b/radicle-cli/src/terminal.rs index ae02ef93..4fd1fcb0 100644 --- a/radicle-cli/src/terminal.rs +++ b/radicle-cli/src/terminal.rs @@ -113,14 +113,13 @@ where /// Get the default profile. Fails if there is no profile. pub fn profile() -> Result { - let error = args::Error::WithHint { - err: anyhow::anyhow!("Could not load radicle profile"), - hint: "To setup your radicle profile, run `rad auth`.", - }; - match Profile::load() { Ok(profile) => Ok(profile), - Err(_) => Err(error.into()), + Err(e) => Err(args::Error::WithHint { + err: anyhow::anyhow!("Could not load radicle profile: {e}"), + hint: "To setup your radicle profile, run `rad auth`.", + } + .into()), } } diff --git a/radicle-cli/src/terminal/args.rs b/radicle-cli/src/terminal/args.rs index c025685b..c3e85de8 100644 --- a/radicle-cli/src/terminal/args.rs +++ b/radicle-cli/src/terminal/args.rs @@ -6,7 +6,7 @@ use anyhow::anyhow; use radicle::cob::{self, issue, patch}; use radicle::crypto; use radicle::git::RefString; -use radicle::node::Address; +use radicle::node::{Address, Alias}; use radicle::prelude::{Did, Id, NodeId}; #[derive(thiserror::Error, Debug)] @@ -136,6 +136,15 @@ pub fn string(val: &OsString) -> String { val.to_string_lossy().to_string() } +pub fn alias(val: &OsString) -> anyhow::Result { + let val = val.as_os_str(); + let val = val + .to_str() + .ok_or_else(|| anyhow!("alias must be valid UTF-8"))?; + + Alias::from_str(val).map_err(|e| e.into()) +} + pub fn issue(val: &OsString) -> anyhow::Result { let val = val.to_string_lossy(); issue::IssueId::from_str(&val).map_err(|_| anyhow!("invalid Issue ID '{}'", val)) diff --git a/radicle-cli/src/terminal/format.rs b/radicle-cli/src/terminal/format.rs index 22a4e4cc..ee544642 100644 --- a/radicle-cli/src/terminal/format.rs +++ b/radicle-cli/src/terminal/format.rs @@ -4,7 +4,7 @@ pub use radicle_term::format::*; pub use radicle_term::{style, Paint}; use radicle::cob::{ObjectId, Timestamp}; -use radicle::node::{AliasStore, NodeId}; +use radicle::node::{Alias, AliasStore, NodeId}; use radicle::prelude::Did; use radicle::profile::Profile; use radicle_term::element::Line; @@ -143,15 +143,15 @@ impl<'a> fmt::Display for Identity<'a> { pub enum Author<'a> { Author { nid: &'a NodeId, - alias: Option, + alias: Option, }, Me { - alias: Option, + alias: Option, }, } impl<'a> Author<'a> { - pub fn new(nid: &'a NodeId, alias: Option, me: &Profile) -> Author<'a> { + pub fn new(nid: &'a NodeId, alias: Option, me: &Profile) -> Author<'a> { if nid == me.id() { Self::Me { alias } } else { diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 4ed71015..5aae15d5 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use std::{env, fs, thread, time}; use radicle::git; +use radicle::node::Alias; use radicle::node::Handle as _; use radicle::prelude::Id; use radicle::profile::Home; @@ -48,6 +49,7 @@ fn test<'a>( .env("EDITOR", "true") .env("TZ", "UTC") .env("LANG", "C") + .env("USER", "alice") .env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284") .envs(git::env::GIT_DEFAULT_CONFIG) .envs(envs) @@ -234,12 +236,12 @@ fn rad_node() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let alice = environment.node("alice"); - let bob = environment.node("bob"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let working = tempfile::tempdir().unwrap(); - let alice = alice.spawn(Config::default()); - let _bob = bob.spawn(Config::default()); + let alice = alice.spawn(); + let _bob = bob.spawn(); fixtures::repository(working.path().join("alice")); @@ -398,9 +400,9 @@ fn rad_rm() { #[test] fn rad_track() { let mut environment = Environment::new(); - let alice = environment.node("alice"); + let alice = environment.node(Config::new(Alias::new("alice"))); let working = tempfile::tempdir().unwrap(); - let alice = alice.spawn(Config::default()); + let alice = alice.spawn(); test( "examples/rad-track.md", @@ -416,15 +418,15 @@ fn rad_clone() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let mut alice = environment.node("alice"); - let bob = environment.node("bob"); + let mut alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let working = environment.tmp().join("working"); // Setup a test project. let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack"); - let mut alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors. alice.handle.track_repo(acme, Scope::Trusted).unwrap(); @@ -438,17 +440,17 @@ fn rad_clone_all() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let mut alice = environment.node("alice"); - let bob = environment.node("bob"); - let eve = environment.node("eve"); + let mut alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); + let eve = environment.node(Config::new(Alias::new("eve"))); let working = environment.tmp().join("working"); // Setup a test project. let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack"); - let mut alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); - let mut eve = eve.spawn(Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + let mut eve = eve.spawn(); alice.handle.track_repo(acme, Scope::All).unwrap(); bob.connect(&alice).converge([&alice]); @@ -477,7 +479,7 @@ fn rad_clone_all() { #[test] fn rad_self() { let mut environment = Environment::new(); - let alice = environment.node("alice"); + let alice = environment.node(Config::new(Alias::new("alice"))); let working = environment.tmp().join("working"); test("examples/rad-self.md", working, Some(&alice.home), []).unwrap(); @@ -488,10 +490,10 @@ fn rad_clone_unknown() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let alice = environment.node("alice"); + let alice = environment.node(Config::new(Alias::new("alice"))); let working = environment.tmp().join("working"); - let alice = alice.spawn(Config::default()); + let alice = alice.spawn(); test( "examples/rad-clone-unknown.md", @@ -507,12 +509,12 @@ fn rad_init_sync_and_clone() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let alice = environment.node("alice"); - let bob = environment.node("bob"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let working = environment.tmp().join("working"); - let alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); + let alice = alice.spawn(); + let mut bob = bob.spawn(); bob.connect(&alice); @@ -548,11 +550,11 @@ fn rad_init_sync_and_clone() { fn rad_fetch() { let mut environment = Environment::new(); let working = environment.tmp().join("working"); - let alice = environment.node("alice"); - let bob = environment.node("bob"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); - let mut alice = alice.spawn(Config::default()); - let bob = bob.spawn(Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); fixtures::repository(working.join("alice")); @@ -582,11 +584,11 @@ fn rad_fetch() { fn rad_fork() { let mut environment = Environment::new(); let working = environment.tmp().join("working"); - let alice = environment.node("alice"); - let bob = environment.node("bob"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); - let mut alice = alice.spawn(Config::default()); - let bob = bob.spawn(Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); fixtures::repository(working.join("alice")); @@ -625,10 +627,10 @@ fn test_clone_without_seeds() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let mut alice = environment.node("alice"); + let mut alice = environment.node(Config::new(Alias::new("alice"))); let working = environment.tmp().join("working"); let rid = alice.project("heartwood", "Radicle Heartwood Protocol & Stack"); - let mut alice = alice.spawn(Config::default()); + let mut alice = alice.spawn(); let seeds = alice.handle.seeds(rid).unwrap(); assert!(!seeds.has_connections()); @@ -648,13 +650,13 @@ fn test_cob_replication() { let mut environment = Environment::new(); let working = tempfile::tempdir().unwrap(); - let mut alice = environment.node("alice"); - let bob = environment.node("bob"); + let mut alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let rid = alice.project("heartwood", ""); - let mut alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); let events = alice.handle.events(); alice.handle.track_node(bob.id, None).unwrap(); @@ -708,13 +710,13 @@ fn test_cob_replication() { fn test_cob_deletion() { let mut environment = Environment::new(); let working = tempfile::tempdir().unwrap(); - let mut alice = environment.node("alice"); - let bob = environment.node("bob"); + let mut alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let rid = alice.project("heartwood", ""); - let mut alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); alice.handle.track_repo(rid, Scope::All).unwrap(); bob.handle.track_repo(rid, Scope::All).unwrap(); @@ -762,9 +764,9 @@ fn rad_sync() { let mut environment = Environment::new(); let working = environment.tmp().join("working"); - let alice = environment.node("alice"); - let bob = environment.node("bob"); - let eve = environment.node("eve"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); + let eve = environment.node(Config::new(Alias::new("eve"))); let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap(); fixtures::repository(working.join("acme")); @@ -777,9 +779,9 @@ fn rad_sync() { ) .unwrap(); - let mut alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); - let mut eve = eve.spawn(Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + let mut eve = eve.spawn(); bob.handle.track_repo(acme, Scope::All).unwrap(); eve.handle.track_repo(acme, Scope::All).unwrap(); @@ -806,19 +808,19 @@ fn rad_sync() { // fn test_replication_via_seed() { let mut environment = Environment::new(); - let alice = environment.node("alice"); - let bob = environment.node("bob"); - let seed = environment.node("seed"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); + let seed = environment.node(Config { + policy: Policy::Track, + scope: Scope::All, + ..Config::new(Alias::new("seed")) + }); let working = environment.tmp().join("working"); let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap(); - let mut alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); - let seed = seed.spawn(Config { - policy: Policy::Track, - scope: Scope::All, - ..Config::default() - }); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + let seed = seed.spawn(); alice.connect(&seed); bob.connect(&seed); @@ -891,8 +893,8 @@ fn test_replication_via_seed() { #[test] fn rad_remote() { let mut environment = Environment::new(); - let alice = environment.node("alice"); - let bob = environment.node("bob"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let working = environment.tmp().join("working"); let home = alice.home.clone(); let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap(); @@ -907,11 +909,11 @@ fn rad_remote() { ) .unwrap(); - let mut alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); alice .handle - .track_node(bob.id, Some("bob".to_owned())) + .track_node(bob.id, Some(Alias::new("bob"))) .unwrap(); bob.connect(&alice); @@ -935,7 +937,7 @@ fn rad_merge_via_push() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let alice = environment.node("alice"); + let alice = environment.node(Config::new(Alias::new("alice"))); let working = environment.tmp().join("working"); fixtures::repository(working.join("alice")); @@ -948,7 +950,7 @@ fn rad_merge_via_push() { ) .unwrap(); - let alice = alice.spawn(Config::default()); + let alice = alice.spawn(); test( "examples/rad-merge-via-push.md", @@ -964,8 +966,8 @@ fn git_push_and_pull() { logger::init(log::Level::Debug); let mut environment = Environment::new(); - let alice = environment.node("alice"); - let bob = environment.node("bob"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let working = environment.tmp().join("working"); fixtures::repository(working.join("alice")); @@ -978,8 +980,8 @@ fn git_push_and_pull() { ) .unwrap(); - let alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config::default()); + let alice = alice.spawn(); + let mut bob = bob.spawn(); bob.connect(&alice).converge([&alice]); @@ -1016,8 +1018,8 @@ fn git_push_and_pull() { #[test] fn rad_workflow() { let mut environment = Environment::new(); - let alice = environment.node("alice"); - let bob = environment.node("bob"); + let alice = environment.node(Config::new(Alias::new("alice"))); + let bob = environment.node(Config::new(Alias::new("bob"))); let working = environment.tmp().join("working"); fixtures::repository(working.join("alice")); @@ -1030,11 +1032,8 @@ fn rad_workflow() { ) .unwrap(); - let alice = alice.spawn(Config::default()); - let mut bob = bob.spawn(Config { - alias: Some("bob".to_string()), - ..Default::default() - }); + let alice = alice.spawn(); + let mut bob = bob.spawn(); bob.connect(&alice).converge([&alice]); diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs index 2b1d7dba..3fa310cc 100644 --- a/radicle-httpd/src/api/json.rs +++ b/radicle-httpd/src/api/json.rs @@ -14,7 +14,7 @@ use radicle::cob::thread; use radicle::cob::thread::CommentId; use radicle::cob::{ActorId, Author, Reaction, Timestamp}; use radicle::git::RefString; -use radicle::node::AliasStore; +use radicle::node::{Alias, AliasStore}; use radicle::prelude::NodeId; use radicle::storage::{git, refs, ReadRepository}; use radicle_surf::blob::Blob; @@ -146,7 +146,7 @@ pub(crate) fn patch( } /// Returns JSON for an `author` and fills in `alias` when present. -fn author(author: &Author, alias: Option) -> Value { +fn author(author: &Author, alias: Option) -> Value { match alias { Some(alias) => json!({ "id": author.id, @@ -157,7 +157,7 @@ fn author(author: &Author, alias: Option) -> Value { } /// Returns JSON for a patch `Merge` and fills in `alias` when present. -fn merge(merge: &Merge, nid: &NodeId, alias: Option) -> Value { +fn merge(merge: &Merge, nid: &NodeId, alias: Option) -> Value { match alias { Some(alias) => json!({ "author": { @@ -180,7 +180,7 @@ fn merge(merge: &Merge, nid: &NodeId, alias: Option) -> Value { } /// Returns JSON for a patch `Review` and fills in `alias` when present. -fn review(nid: &NodeId, alias: Option, review: &Review) -> Value { +fn review(nid: &NodeId, alias: Option, review: &Review) -> Value { match alias { Some(alias) => json!({ "author": { diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index aad3054d..82967df0 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -16,9 +16,11 @@ use radicle::crypto::ssh::keystore::MemorySigner; use radicle::crypto::ssh::Keystore; use radicle::crypto::{KeyPair, Seed, Signer}; use radicle::git::{raw as git2, RefString}; +use radicle::node; use radicle::node::address as AddressStore; use radicle::node::routing as RoutingStore; use radicle::node::tracking::store as TrackingStore; +use radicle::profile; use radicle::profile::Home; use radicle::storage::ReadStorage; use radicle::Storage; @@ -63,6 +65,9 @@ pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile { storage, keystore, public_key: keypair.pk.into(), + config: profile::Config { + node: node::Config::new(node::Alias::new("seed")), + }, } } diff --git a/radicle-node/src/control.rs b/radicle-node/src/control.rs index c3f180d3..4df4bc8b 100644 --- a/radicle-node/src/control.rs +++ b/radicle-node/src/control.rs @@ -13,7 +13,7 @@ use serde_json as json; use crate::identity::Id; use crate::node::NodeId; -use crate::node::{Command, CommandName, CommandResult}; +use crate::node::{Alias, Command, CommandName, CommandResult}; use crate::runtime; use crate::runtime::thread; @@ -150,6 +150,12 @@ where [node, alias] => (node.as_str(), Some(alias.to_owned())), _ => return Err(CommandError::InvalidCommandArgs(cmd.args)), }; + let alias = alias + .map(|a| { + Alias::from_str(&a) + .map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e))) + }) + .transpose()?; let nid = node .parse() .map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e)))?; @@ -358,12 +364,8 @@ mod tests { assert!(handle.untrack_repo(proj).unwrap()); assert!(!handle.untrack_repo(proj).unwrap()); - assert!(handle - .track_node(peer, Some(String::from("alice"))) - .unwrap()); - assert!(!handle - .track_node(peer, Some(String::from("alice"))) - .unwrap()); + assert!(handle.track_node(peer, Some(Alias::new("alice"))).unwrap()); + assert!(!handle.track_node(peer, Some(Alias::new("alice"))).unwrap()); assert!(handle.untrack_node(peer).unwrap()); assert!(!handle.untrack_node(peer).unwrap()); } diff --git a/radicle-node/src/main.rs b/radicle-node/src/main.rs index f5b33333..16cb8689 100644 --- a/radicle-node/src/main.rs +++ b/radicle-node/src/main.rs @@ -5,11 +5,11 @@ use crossbeam_channel as chan; use cyphernet::addr::PeerAddr; use localtime::LocalDuration; +use radicle::node; use radicle::prelude::Signer; use radicle::profile; use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner}; use radicle_node::prelude::{Address, NodeId}; -use radicle_node::service::tracking::{Policy, Scope}; use radicle_node::Runtime; use radicle_node::{logger, service, signals}; use radicle_term as term; @@ -21,7 +21,6 @@ Usage Options - --alias Identify yourself with an alias on the network --connect Connect to the given peer address on start --external-address
Publicly accessible address (default 0.0.0.0:8776) --git-daemon
Address to bind git-daemon to (default 0.0.0.0:9418) @@ -34,48 +33,29 @@ Options #[derive(Debug)] struct Options { - alias: Option, - connect: Vec<(NodeId, Address)>, - external_addresses: Vec
, daemon: Option, - limits: service::config::Limits, listen: Vec, force: bool, - tracking_policy: Policy, - tracking_scope: Scope, } impl Options { - fn from_env() -> Result { + fn from_env(config: &mut node::Config) -> Result { use lexopt::prelude::*; let mut parser = lexopt::Parser::from_env(); - let mut alias = None; - let mut connect = Vec::new(); - let mut external_addresses = Vec::new(); - let mut limits = service::config::Limits::default(); let mut listen = Vec::new(); let mut daemon = None; - let mut tracking_policy = Policy::default(); - let mut tracking_scope = Scope::default(); let mut force = false; while let Some(arg) = parser.next()? { match arg { - Long("alias") => { - let name: String = parser.value()?.parse()?; - if name.len() > 32 { - anyhow::bail!("alias '{}' is longer than 32 characters", name); - } - alias = Some(name); - } Long("connect") => { let peer: PeerAddr = parser.value()?.parse()?; - connect.push((peer.id, peer.addr.clone())); + config.connect.push(peer.into()); } Long("external-address") => { let addr = parser.value()?.parse()?; - external_addresses.push(addr); + config.external_addresses.push(addr); } Long("force") => { force = true; @@ -89,24 +69,24 @@ impl Options { .value()? .parse() .map_err(|s| anyhow!("unknown tracking policy {:?}", s))?; - tracking_policy = policy; + config.policy = policy; } Long("tracking-scope") => { let scope = parser .value()? .parse() .map_err(|s| anyhow!("unknown tracking scope {:?}", s))?; - tracking_scope = scope; + config.scope = scope; } Long("limit-routing-max-age") => { let secs: u64 = parser.value()?.parse()?; - limits.routing_max_age = LocalDuration::from_secs(secs); + config.limits.routing_max_age = LocalDuration::from_secs(secs); } Long("limit-routing-max-size") => { - limits.routing_max_size = parser.value()?.parse()?; + config.limits.routing_max_size = parser.value()?.parse()?; } Long("limit-fetch-concurrency") => { - limits.fetch_concurrency = parser.value()?.parse()?; + config.limits.fetch_concurrency = parser.value()?.parse()?; } Long("listen") => { let addr = parser.value()?.parse()?; @@ -120,7 +100,7 @@ impl Options { } } - if external_addresses.len() > service::ADDRESS_LIMIT { + if config.external_addresses.len() > service::ADDRESS_LIMIT { anyhow::bail!( "external address limit ({}) exceeded", service::ADDRESS_LIMIT, @@ -128,15 +108,9 @@ impl Options { } Ok(Self { - alias, - connect, daemon, - external_addresses, force, - limits, listen, - tracking_policy, - tracking_scope, }) } } @@ -144,13 +118,14 @@ impl Options { fn execute() -> anyhow::Result<()> { logger::init(log::Level::Debug)?; - let options = Options::from_env()?; - log::info!(target: "node", "Starting node.."); log::info!(target: "node", "Version {} ({})", env!("CARGO_PKG_VERSION"), env!("GIT_HEAD")); - log::info!(target: "node", "Unlocking node keystore.."); let home = profile::home()?; + let mut config = profile::Config::load(&home.config())?.node; + + log::info!(target: "node", "Unlocking node keystore.."); + let passphrase = term::io::passphrase(profile::env::RAD_PASSPHRASE) .context(format!("`{}` must be set", profile::env::RAD_PASSPHRASE))?; let keystore = Keystore::new(&home.keys()); @@ -158,15 +133,7 @@ fn execute() -> anyhow::Result<()> { log::info!(target: "node", "Node ID is {}", signer.public_key()); - let config = service::Config { - alias: options.alias, - connect: options.connect.into_iter().collect(), - external_addresses: options.external_addresses, - limits: options.limits, - policy: options.tracking_policy, - scope: options.tracking_scope, - ..service::Config::default() - }; + let options = Options::from_env(&mut config)?; let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050); let daemon = options.daemon.unwrap_or_else(|| { net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), radicle::git::PROTOCOL_PORT) diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs index c8c4f177..76bd4ee7 100644 --- a/radicle-node/src/runtime.rs +++ b/radicle-node/src/runtime.rs @@ -154,7 +154,10 @@ impl Runtime { .ok() .and_then(|ann| NodeAnnouncement::decode(&mut ann.as_slice()).ok()) .and_then(|ann| { - if config.matches(&ann) { + if config.features() == ann.features + && config.alias == ann.alias + && config.external_addresses == ann.addresses.as_ref() + { Some(ann) } else { None @@ -168,8 +171,7 @@ impl Runtime { ); ann } else { - config - .node(clock.as_secs()) + service::gossip::node(&config, clock.as_secs()) .solve(Default::default()) .expect("Runtime::init: unable to solve proof-of-work puzzle") }; diff --git a/radicle-node/src/runtime/handle.rs b/radicle-node/src/runtime/handle.rs index d27b3380..830417a1 100644 --- a/radicle-node/src/runtime/handle.rs +++ b/radicle-node/src/runtime/handle.rs @@ -9,7 +9,7 @@ use reactor::poller::popol::PopolWaker; use thiserror::Error; use crate::identity::Id; -use crate::node::{Command, FetchResult}; +use crate::node::{Alias, Command, FetchResult}; use crate::profile::Home; use crate::runtime::Emitter; use crate::service; @@ -161,7 +161,7 @@ impl radicle::node::Handle for Handle { receiver.recv().map_err(Error::from) } - fn track_node(&mut self, id: NodeId, alias: Option) -> Result { + fn track_node(&mut self, id: NodeId, alias: Option) -> Result { let (sender, receiver) = chan::bounded(1); self.command(service::Command::TrackNode(id, alias, sender))?; receiver.recv().map_err(Error::from) diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index 1379ed13..e5eb5c12 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -1,7 +1,6 @@ #![allow(clippy::too_many_arguments)] #![allow(clippy::collapsible_match)] #![allow(clippy::collapsible_if)] -pub mod config; pub mod filter; pub mod io; pub mod message; @@ -28,7 +27,7 @@ use crate::identity::IdentityError; use crate::identity::{Doc, Id}; use crate::node::routing; use crate::node::routing::InsertResult; -use crate::node::{Address, Features, FetchResult, Seed, Seeds}; +use crate::node::{Address, Alias, Features, FetchResult, Seed, Seeds}; use crate::prelude::*; use crate::runtime::Emitter; use crate::service::message::{Announcement, AnnouncementMessage, Ping}; @@ -41,8 +40,7 @@ use crate::worker::FetchError; use crate::Link; pub use crate::node::events::{Event, Events}; -pub use crate::node::NodeId; -pub use crate::service::config::{Config, Network}; +pub use crate::node::{config::Network, Config, NodeId}; pub use crate::service::message::{Message, ZeroBytes}; pub use crate::service::session::Session; @@ -137,7 +135,7 @@ pub enum Command { /// Untrack the given repository. UntrackRepo(Id, chan::Sender), /// Track the given node. - TrackNode(NodeId, Option, chan::Sender), + TrackNode(NodeId, Option, chan::Sender), /// Untrack the given node. UntrackNode(NodeId, chan::Sender), /// Query the internal service state. @@ -382,7 +380,7 @@ where // Connect to configured peers. let addrs = self.config.connect.clone(); - for (id, addr) in addrs { + for (id, addr) in addrs.into_iter().map(|ca| ca.into()) { self.connect(id, addr); } // Ensure that our inventory is recorded in our routing table, and we are tracking @@ -405,7 +403,7 @@ where .insert( &self.node_id(), self.node.features, - self.node.alias().unwrap_or_default(), + self.node.alias.clone(), self.node.work(), self.node.timestamp, self.node @@ -998,14 +996,6 @@ where return Ok(false); } - let alias = match ann.alias() { - Ok(s) => s, - Err(e) => { - warn!(target: "service", "Dropping node announcement from {announcer}: invalid alias: {e}"); - return Ok(false); - } - }; - // If this node isn't a seed, we're not interested in adding it // to our address book, but other nodes may be, so we relay the message anyway. if !features.has(Features::SEED) { @@ -1015,7 +1005,7 @@ where match self.addresses.insert( announcer, *features, - alias, + ann.alias.clone(), ann.work(), timestamp, addresses @@ -1781,7 +1771,7 @@ impl DerefMut for Sessions { } } -mod gossip { +pub mod gossip { use super::*; use crate::service::filter::Filter; @@ -1842,6 +1832,24 @@ mod gossip { ] } + pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement { + let features = config.features(); + let alias = config.alias.clone(); + let addresses: BoundedVec<_, ADDRESS_LIMIT> = config + .external_addresses + .clone() + .try_into() + .expect("external addresses are within the limit"); + + NodeAnnouncement { + features, + timestamp, + alias, + addresses, + nonce: 0, + } + } + pub fn inventory(timestamp: Timestamp, inventory: Vec) -> InventoryAnnouncement { type Inventory = BoundedVec; diff --git a/radicle-node/src/service/config.rs b/radicle-node/src/service/config.rs deleted file mode 100644 index 57693c67..00000000 --- a/radicle-node/src/service/config.rs +++ /dev/null @@ -1,147 +0,0 @@ -use localtime::LocalDuration; - -use radicle::node; -use radicle::node::Address; - -use crate::bounded::BoundedVec; -use crate::service::message::{NodeAnnouncement, ADDRESS_LIMIT}; -use crate::service::tracking::{Policy, Scope}; -use crate::service::NodeId; - -/// Peer-to-peer network. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum Network { - #[default] - Main, - Test, -} - -/// Configuration parameters defining attributes of minima and maxima. -#[derive(Debug, Clone)] -pub struct Limits { - /// Number of routing table entries before we start pruning. - pub routing_max_size: usize, - /// How long to keep a routing table entry before being pruned. - pub routing_max_age: LocalDuration, - /// Maximum number of concurrent fetches per per connection. - pub fetch_concurrency: usize, -} - -impl Default for Limits { - fn default() -> Self { - Self { - routing_max_size: 1000, - routing_max_age: LocalDuration::from_mins(7 * 24 * 60), - fetch_concurrency: 1, - } - } -} - -/// Service configuration. -#[derive(Debug, Clone)] -pub struct Config { - /// Alias chosen by the operator. - /// Doesn't have to be unique on the network. - pub alias: Option, - /// Peers to connect to on startup. - /// Connections to these peers will be maintained. - pub connect: Vec<(NodeId, Address)>, - /// Specify the node's public addresses - pub external_addresses: Vec
, - /// Peer-to-peer network. - pub network: Network, - /// Whether or not our node should relay inventories. - pub relay: bool, - /// Configured service limits. - pub limits: Limits, - /// Default tracking policy. - pub policy: Policy, - /// Default tracking scope. - pub scope: Scope, -} - -impl Default for Config { - fn default() -> Self { - Self { - alias: None, - connect: Vec::default(), - external_addresses: vec![], - network: Network::default(), - relay: true, - limits: Limits::default(), - policy: Policy::default(), - scope: Scope::default(), - } - } -} - -impl Config { - pub fn new(network: Network) -> Self { - Self { - network, - ..Self::default() - } - } - - pub fn peer(&self, id: &NodeId) -> Option<&Address> { - self.connect.iter().find(|(i, _)| i == id).map(|(_, a)| a) - } - - pub fn is_persistent(&self, id: &NodeId) -> bool { - self.connect.iter().any(|(i, _)| i == id) - } - - pub fn features(&self) -> node::Features { - node::Features::SEED - } - - /// Check if a node announcement matches this configuration. - pub fn matches(&self, other: &NodeAnnouncement) -> bool { - let ann = self.node(other.timestamp); - - ann.features == other.features - && ann.alias == other.alias - && ann.addresses == other.addresses - } - - pub fn alias(&self) -> [u8; 32] { - let mut alias = [0u8; 32]; - - if let Some(name) = &self.alias { - alias[..name.len()].copy_from_slice(name.as_bytes()); - } - alias - } - - pub fn node(&self, timestamp: node::Timestamp) -> NodeAnnouncement { - let features = self.features(); - let alias = self.alias(); - let addresses: BoundedVec<_, ADDRESS_LIMIT> = self - .external_addresses - .clone() - .try_into() - .expect("external addresses are within the limit"); - - NodeAnnouncement { - features, - timestamp, - alias, - addresses, - nonce: 0, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_node_announcement() { - let cfg = Config { - alias: Some(String::from("cloudhead")), - ..Config::default() - }; - assert_eq!("cloudhead", cfg.node(0).alias().unwrap()); - } -} diff --git a/radicle-node/src/service/message.rs b/radicle-node/src/service/message.rs index 872b2698..c242508b 100644 --- a/radicle-node/src/service/message.rs +++ b/radicle-node/src/service/message.rs @@ -1,10 +1,10 @@ -use std::{fmt, io, mem, str}; +use std::{fmt, io, mem}; use crate::crypto; use crate::crypto::Unverified; use crate::identity::Id; use crate::node; -use crate::node::Address; +use crate::node::{Address, Alias}; use crate::prelude::BoundedVec; use crate::service::filter::Filter; use crate::service::{Link, NodeId, Timestamp}; @@ -57,8 +57,8 @@ pub struct NodeAnnouncement { pub features: node::Features, /// Monotonic timestamp. pub timestamp: Timestamp, - /// Non-unique alias. Must be valid UTF-8. - pub alias: [u8; 32], + /// Non-unique alias. + pub alias: Alias, /// Announced addresses. pub addresses: BoundedVec, /// Nonce used for announcement proof-of-work. @@ -118,11 +118,6 @@ impl NodeAnnouncement { } Some(self) } - - /// Get the alias as a UTF-8 string. - pub fn alias(&self) -> Result<&str, std::str::Utf8Error> { - Ok(str::from_utf8(&self.alias)?.trim_end_matches(0 as char)) - } } impl wire::Encode for NodeAnnouncement { @@ -619,7 +614,7 @@ mod tests { let ann = NodeAnnouncement { features: node::Features::SEED, timestamp: 42491841, - alias: [0; 32], + alias: Alias::new("alice"), addresses: BoundedVec::new(), nonce: 0, }; @@ -627,6 +622,6 @@ mod tests { assert_eq!(ann.work(), 0); assert_eq!(ann.clone().solve(1).unwrap().work(), 4); assert_eq!(ann.clone().solve(8).unwrap().work(), 9); - assert_eq!(ann.solve(14).unwrap().work(), 16); + assert_eq!(ann.solve(14).unwrap().work(), 14); } } diff --git a/radicle-node/src/service/session.rs b/radicle-node/src/service/session.rs index 3479c46c..b17f83aa 100644 --- a/radicle-node/src/service/session.rs +++ b/radicle-node/src/service/session.rs @@ -1,7 +1,7 @@ use std::collections::{HashSet, VecDeque}; use std::fmt; -use crate::service::config::Limits; +use crate::node::config::Limits; use crate::service::message; use crate::service::message::Message; use crate::service::{Address, Id, LocalTime, NodeId, Outbox, Rng}; diff --git a/radicle-node/src/test/arbitrary.rs b/radicle-node/src/test/arbitrary.rs index 1481a820..468fc2ad 100644 --- a/radicle-node/src/test/arbitrary.rs +++ b/radicle-node/src/test/arbitrary.rs @@ -2,6 +2,7 @@ use bloomy::BloomFilter; use qcheck::Arbitrary; use crate::crypto; +use crate::node::Alias; use crate::prelude::{BoundedVec, Id, NodeId, Timestamp}; use crate::service::filter::{Filter, FILTER_SIZE_L, FILTER_SIZE_M, FILTER_SIZE_S}; use crate::service::message::{ @@ -65,7 +66,7 @@ impl Arbitrary for Message { let message = NodeAnnouncement { features: u64::arbitrary(g).into(), timestamp: Timestamp::arbitrary(g), - alias: <[u8; 32]>::arbitrary(g), + alias: Alias::arbitrary(g), addresses: Arbitrary::arbitrary(g), nonce: u64::arbitrary(g), } diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index 1b484f67..c8962549 100644 --- a/radicle-node/src/test/environment.rs +++ b/radicle-node/src/test/environment.rs @@ -1,6 +1,7 @@ use std::io::BufRead as _; use std::mem::ManuallyDrop; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::{ collections::{BTreeMap, BTreeSet}, env, fs, io, iter, net, process, thread, time, @@ -21,7 +22,8 @@ use radicle::node::address::Book; use radicle::node::routing::Store; use radicle::node::tracking::store as TrackingStore; use radicle::node::Handle as _; -use radicle::node::{ADDRESS_DB_FILE, TRACKING_DB_FILE}; +use radicle::node::{Alias, ADDRESS_DB_FILE, TRACKING_DB_FILE}; +use radicle::profile; use radicle::profile::Home; use radicle::profile::Profile; use radicle::rad; @@ -74,8 +76,8 @@ impl Environment { /// Create a new node in this environment. This should be used when a running node /// is required. Use [`Environment::profile`] otherwise. - pub fn node(&mut self, name: &str) -> Node { - let profile = self.profile(name); + pub fn node(&mut self, config: Config) -> Node { + let profile = self.profile(&config.alias); let signer = MemorySigner::load(&profile.keystore, "radicle".to_owned().into()).unwrap(); let tracking_db = profile.home.node().join(TRACKING_DB_FILE); TrackingStore::Config::open(tracking_db).unwrap(); @@ -85,6 +87,7 @@ impl Environment { Node { id: *profile.id(), home: profile.home, + config, signer, storage: profile.storage, } @@ -92,12 +95,15 @@ impl Environment { /// Create a new profile in this environment. /// This should be used when a running node is not required. - pub fn profile(&mut self, name: &str) -> Profile { - let home = Home::new(self.tmp().join("home").join(name).join(".radicle")).unwrap(); + pub fn profile(&mut self, alias: &str) -> Profile { + let home = Home::new(self.tmp().join("home").join(alias).join(".radicle")).unwrap(); let storage = Storage::open(home.storage()).unwrap(); let keystore = Keystore::new(&home.keys()); let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32])); let tracking_db = home.node().join(TRACKING_DB_FILE); + let alias = Alias::from_str(alias).unwrap(); + let config = profile::Config::init(alias, &home.config()).unwrap(); + TrackingStore::Config::open(tracking_db).unwrap(); let addresses_db = home.node().join(ADDRESS_DB_FILE); Book::open(addresses_db).unwrap(); @@ -115,6 +121,7 @@ impl Environment { storage, keystore, public_key: keypair.pk.into(), + config, } } } @@ -125,6 +132,7 @@ pub struct Node { pub home: Home, pub signer: G, pub storage: Storage, + pub config: Config, } /// Handle to a running node. @@ -303,7 +311,7 @@ impl NodeHandle { impl Node { /// Create a new node. - pub fn init(base: &Path) -> Self { + pub fn init(base: &Path, config: Config) -> Self { let home = base.join( iter::repeat_with(fastrand::alphanumeric) .take(8) @@ -318,13 +326,14 @@ impl Node { home, signer, storage, + config, } } } impl + Signer + Clone> Node { /// Spawn a node in its own thread. - pub fn spawn(self, config: service::Config) -> NodeHandle { + pub fn spawn(self) -> NodeHandle { let listen = vec![([0, 0, 0, 0], 0).into()]; let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050); let daemon: net::SocketAddr = { @@ -337,7 +346,7 @@ impl + Signer + Clone> Node { let (_, signals) = chan::bounded(1); let rt = Runtime::init( self.home.clone(), - config, + self.config, listen, proxy, daemon, diff --git a/radicle-node/src/test/handle.rs b/radicle-node/src/test/handle.rs index 08ab76ae..e0c313a9 100644 --- a/radicle-node/src/test/handle.rs +++ b/radicle-node/src/test/handle.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use std::{io, time}; use crate::identity::Id; -use crate::node::{Event, FetchResult, Seeds}; +use crate::node::{Alias, Event, FetchResult, Seeds}; use crate::runtime::HandleError; use crate::service::tracking; use crate::service::NodeId; @@ -51,7 +51,7 @@ impl radicle::node::Handle for Handle { Ok(self.tracking_repos.lock().unwrap().remove(&id)) } - fn track_node(&mut self, id: NodeId, _alias: Option) -> Result { + fn track_node(&mut self, id: NodeId, _alias: Option) -> Result { Ok(self.tracking_nodes.lock().unwrap().insert(id)) } diff --git a/radicle-node/src/test/peer.rs b/radicle-node/src/test/peer.rs index 91f5fcf3..fc6dac4c 100644 --- a/radicle-node/src/test/peer.rs +++ b/radicle-node/src/test/peer.rs @@ -2,11 +2,12 @@ use std::iter; use std::net; use std::ops::{Deref, DerefMut}; +use std::str::FromStr; use log::*; -use radicle::node::address; use radicle::node::address::Store; +use radicle::node::{address, Alias}; use radicle::rad; use radicle::storage::ReadRepository; use radicle::Storage; @@ -110,7 +111,7 @@ impl Default for Config { let signer = MockSigner::new(&mut rng); Config { - config: service::Config::default(), + config: service::Config::new(Alias::from_str("mocky").unwrap()), addrs: address::Book::memory().unwrap(), local_time: LocalTime::now(), policy: Policy::default(), @@ -162,7 +163,7 @@ where // Make sure the peer address is advertized. config.config.external_addresses.push(local_addr.into()); - let announcement = config.config.node(config.local_time.as_secs()); + let announcement = service::gossip::node(&config.config, config.local_time.as_secs()); let emitter: Emitter = Default::default(); let service = Service::new( config.config, @@ -214,7 +215,7 @@ where .insert( &peer.node_id(), radicle::node::Features::default(), - peer.name, + Alias::from_str(peer.name).unwrap(), 0, timestamp, Some(known_address), @@ -258,14 +259,11 @@ where } pub fn node_announcement(&self) -> Message { - let mut alias = [0u8; 32]; - alias[..self.name.len()].copy_from_slice(self.name.as_bytes()); - Message::node( NodeAnnouncement { features: node::Features::SEED, timestamp: self.timestamp(), - alias, + alias: Alias::from_str(self.name).unwrap(), addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(), nonce: 0, } diff --git a/radicle-node/src/tests.rs b/radicle-node/src/tests.rs index 0f6447bf..10e2b5a7 100644 --- a/radicle-node/src/tests.rs +++ b/radicle-node/src/tests.rs @@ -14,9 +14,9 @@ use crate::collections::{HashMap, HashSet}; use crate::crypto::test::signer::MockSigner; use crate::identity::Id; use crate::node; +use crate::node::config::*; use crate::prelude::*; use crate::prelude::{LocalDuration, Timestamp}; -use crate::service::config::*; use crate::service::filter::Filter; use crate::service::io::Io; use crate::service::message::*; @@ -208,8 +208,11 @@ fn test_persistent_peer_connect() { MockStorage::empty(), peer::Config { config: Config { - connect: vec![(bob.id(), bob.address()), (eve.id(), eve.address())], - ..Config::default() + connect: vec![ + (bob.id(), bob.address()).into(), + (eve.id(), eve.address()).into(), + ], + ..Config::new(node::Alias::new("alice")) }, ..peer::Config::default() }, @@ -320,7 +323,7 @@ fn test_inventory_pruning() { peer::Config { config: Config { limits: test.limits, - ..Config::default() + ..Config::new(node::Alias::new("alice")) }, ..peer::Config::default() }, @@ -753,7 +756,11 @@ fn test_refs_announcement_trusted() { // Alice starts to track Bob. let (sender, receiver) = chan::bounded(1); - alice.command(Command::TrackNode(bob.id, Some("bob".to_string()), sender)); + alice.command(Command::TrackNode( + bob.id, + Some(node::Alias::new("bob")), + sender, + )); let policy_change = receiver.recv().map_err(runtime::HandleError::from).unwrap(); assert!(policy_change); @@ -886,8 +893,11 @@ fn test_persistent_peer_reconnect_attempt() { MockStorage::empty(), peer::Config { config: Config { - connect: vec![(bob.id(), bob.address()), (eve.id(), eve.address())], - ..Config::default() + connect: vec![ + (bob.id(), bob.address()).into(), + (eve.id(), eve.address()).into(), + ], + ..Config::new(node::Alias::new("alice")) }, ..peer::Config::default() }, @@ -944,8 +954,8 @@ fn test_persistent_peer_reconnect_success() { MockStorage::empty(), peer::Config { config: Config { - connect: vec![(bob.id, bob.addr())], - ..Config::default() + connect: vec![(bob.id, bob.addr()).into()], + ..Config::new(node::Alias::new("alice")) }, ..peer::Config::default() }, diff --git a/radicle-node/src/tests/e2e.rs b/radicle-node/src/tests/e2e.rs index bfe2f0e6..b5f46332 100644 --- a/radicle-node/src/tests/e2e.rs +++ b/radicle-node/src/tests/e2e.rs @@ -2,13 +2,14 @@ use std::{collections::HashSet, thread, time}; use radicle::crypto::{test::signer::MockSigner, Signer}; use radicle::git; -use radicle::node::{FetchResult, Handle as _}; +use radicle::node::{Alias, FetchResult, Handle as _}; use radicle::storage::{ReadRepository, ReadStorage, WriteRepository, WriteStorage}; use radicle::test::fixtures; use radicle::{assert_matches, rad}; +use crate::node::config::Limits; +use crate::node::Config; use crate::service; -use crate::service::config::Limits; use crate::service::tracking::Scope; use crate::storage::git::transport; use crate::test::environment::{converge, Environment, Node}; @@ -23,14 +24,14 @@ fn test_inventory_sync_basic() { let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); alice.project("alice", ""); bob.project("bob", ""); - let mut alice = alice.spawn(service::Config::default()); - let bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); @@ -47,17 +48,17 @@ fn test_inventory_sync_bridge() { let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); - let mut eve = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); + let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve"))); alice.project("alice", ""); bob.project("bob", ""); eve.project("eve", ""); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); - let eve = eve.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + let eve = eve.spawn(); alice.connect(&bob); bob.connect(&eve); @@ -77,20 +78,20 @@ fn test_inventory_sync_ring() { let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); - let mut eve = Node::init(tmp.path()); - let mut carol = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); + let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve"))); + let mut carol = Node::init(tmp.path(), Config::new(Alias::new("carol"))); alice.project("alice", ""); bob.project("bob", ""); eve.project("eve", ""); carol.project("carol", ""); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); - let mut eve = eve.spawn(service::Config::default()); - let mut carol = carol.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + let mut eve = eve.spawn(); + let mut carol = carol.spawn(); alice.connect(&bob); bob.connect(&eve); @@ -114,11 +115,11 @@ fn test_inventory_sync_star() { let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); - let mut eve = Node::init(tmp.path()); - let mut carol = Node::init(tmp.path()); - let mut dave = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); + let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve"))); + let mut carol = Node::init(tmp.path(), Config::new(Alias::new("carol"))); + let mut dave = Node::init(tmp.path(), Config::new(Alias::new("dave"))); alice.project("alice", ""); bob.project("bob", ""); @@ -126,11 +127,11 @@ fn test_inventory_sync_star() { carol.project("carol", ""); dave.project("dave", ""); - let alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); - let mut eve = eve.spawn(service::Config::default()); - let mut carol = carol.spawn(service::Config::default()); - let mut dave = dave.spawn(service::Config::default()); + let alice = alice.spawn(); + let mut bob = bob.spawn(); + let mut eve = eve.spawn(); + let mut carol = carol.spawn(); + let mut dave = dave.spawn(); bob.connect(&alice); eve.connect(&alice); @@ -146,12 +147,12 @@ fn test_replication() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); + let alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = bob.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -203,8 +204,8 @@ fn test_replication_no_delegates() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); + let alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = bob.project("acme", ""); // Delete one of the signed refs. @@ -216,8 +217,8 @@ fn test_replication_no_delegates() { .delete() .unwrap(); - let mut alice = alice.spawn(service::Config::default()); - let bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -236,8 +237,8 @@ fn test_replication_no_delegates() { #[test] fn test_replication_invalid() { let tmp = tempfile::tempdir().unwrap(); - let alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); + let alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let carol = MockSigner::default(); let acme = bob.project("acme", ""); let repo = bob.storage.repository_mut(acme).unwrap(); @@ -262,8 +263,8 @@ fn test_replication_invalid() { ) .unwrap(); - let mut alice = alice.spawn(service::Config::default()); - let bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -289,12 +290,12 @@ fn test_migrated_clone() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let bob = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = alice.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -339,12 +340,12 @@ fn test_dont_fetch_owned_refs() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let bob = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = alice.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -366,8 +367,8 @@ fn test_fetch_trusted_remotes() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let bob = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = alice.project("acme", ""); let mut signers = Vec::with_capacity(5); { @@ -378,8 +379,8 @@ fn test_fetch_trusted_remotes() { } } - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -421,12 +422,12 @@ fn test_missing_remote() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let bob = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = alice.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); let carol = MockSigner::default(); alice.connect(&bob); @@ -450,11 +451,11 @@ fn test_fetch_preserve_owned_refs() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let bob = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = alice.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -496,12 +497,12 @@ fn test_clone() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); + let alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = bob.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -554,12 +555,12 @@ fn test_fetch_up_to_date() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); + let alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); let acme = bob.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); alice.connect(&bob); converge([&alice, &bob]); @@ -584,8 +585,8 @@ fn test_large_fetch() { let env = Environment::new(); let scale = env.scale(); - let mut alice = Node::init(&env.tmp()); - let bob = Node::init(&env.tmp()); + let mut alice = Node::init(&env.tmp(), Config::new(Alias::new("alice"))); + let bob = Node::init(&env.tmp(), Config::new(Alias::new("bob"))); let tmp = tempfile::tempdir().unwrap(); let (repo, _) = fixtures::repository(tmp.path()); @@ -593,8 +594,8 @@ fn test_large_fetch() { let rid = alice.project_from("acme", "", &repo); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); let bob_events = bob.handle.events(); bob.handle.track_repo(rid, Scope::All).unwrap(); @@ -619,11 +620,28 @@ fn test_concurrent_fetches() { let env = Environment::new(); let scale = env.scale(); + let repos = scale.max(4); + let limits = Limits { + // Have one fetch be queued. + fetch_concurrency: repos - 1, + ..Limits::default() + }; let mut bob_repos = HashSet::new(); let mut alice_repos = HashSet::new(); - let mut alice = Node::init(&env.tmp()); - let mut bob = Node::init(&env.tmp()); - let repos = scale.max(4); + let mut alice = Node::init( + &env.tmp(), + service::Config { + limits: limits.clone(), + ..service::Config::new(Alias::new("alice")) + }, + ); + let mut bob = Node::init( + &env.tmp(), + service::Config { + limits, + ..service::Config::new(Alias::new("bob")) + }, + ); for i in 0..repos { // Create a repo for Alice. @@ -643,16 +661,8 @@ fn test_concurrent_fetches() { bob_repos.insert(rid); } - let config = service::Config { - limits: Limits { - // Have one fetch be queued. - fetch_concurrency: repos - 1, - ..Limits::default() - }, - ..service::Config::default() - }; - let mut alice = alice.spawn(config.clone()); - let mut bob = bob.spawn(config); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); let alice_events = alice.handle.events(); let bob_events = bob.handle.events(); @@ -718,11 +728,11 @@ fn test_connection_crossing() { logger::init(log::Level::Debug); let tmp = tempfile::tempdir().unwrap(); - let alice = Node::init(tmp.path()); - let bob = Node::init(tmp.path()); + let alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); alice.handle.connect(bob.id, bob.addr.into()).unwrap(); bob.handle.connect(alice.id, alice.addr.into()).unwrap(); @@ -753,15 +763,15 @@ fn test_non_fastforward_sigrefs() { let tmp = tempfile::tempdir().unwrap(); - let alice = Node::init(tmp.path()); - let mut bob = Node::init(tmp.path()); - let eve = Node::init(tmp.path()); + let alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); + let eve = Node::init(tmp.path(), Config::new(Alias::new("eve"))); let rid = bob.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let bob = bob.spawn(service::Config::default()); - let mut eve = eve.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let bob = bob.spawn(); + let mut eve = eve.spawn(); alice.handle.track_repo(rid, Scope::All).unwrap(); eve.handle.track_repo(rid, Scope::All).unwrap(); @@ -805,15 +815,15 @@ fn test_outdated_sigrefs() { let tmp = tempfile::tempdir().unwrap(); - let mut alice = Node::init(tmp.path()); - let bob = Node::init(tmp.path()); - let eve = Node::init(tmp.path()); + let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice"))); + let bob = Node::init(tmp.path(), Config::new(Alias::new("bob"))); + let eve = Node::init(tmp.path(), Config::new(Alias::new("eve"))); let rid = alice.project("acme", ""); - let mut alice = alice.spawn(service::Config::default()); - let mut bob = bob.spawn(service::Config::default()); - let mut eve = eve.spawn(service::Config::default()); + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + let mut eve = eve.spawn(); bob.handle.track_repo(rid, Scope::All).unwrap(); eve.handle.track_repo(rid, Scope::All).unwrap(); @@ -832,7 +842,7 @@ fn test_outdated_sigrefs() { alice .handle - .track_node(eve.id, Some("eve".to_string())) + .track_node(eve.id, Some(Alias::new("eve"))) .unwrap(); alice.handle.fetch(rid, eve.id).unwrap(); let repo = alice.storage.repository(rid).unwrap(); @@ -873,7 +883,7 @@ fn test_outdated_sigrefs() { alice .handle - .track_node(bob.id, Some("bob".to_string())) + .track_node(bob.id, Some(Alias::new("bob"))) .unwrap(); assert_matches!( alice.handle.fetch(rid, bob.id).unwrap(), diff --git a/radicle-node/src/wire.rs b/radicle-node/src/wire.rs index 81c04ce3..8f535580 100644 --- a/radicle-node/src/wire.rs +++ b/radicle-node/src/wire.rs @@ -10,6 +10,7 @@ pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter}; use std::collections::BTreeMap; use std::convert::TryFrom; use std::ops::Deref; +use std::str::FromStr; use std::string::FromUtf8Error; use std::{io, mem}; @@ -20,6 +21,7 @@ use crate::git; use crate::git::fmt; use crate::identity::Id; use crate::node; +use crate::node::Alias; use crate::prelude::*; use crate::service::filter; use crate::storage::refs::Refs; @@ -47,6 +49,8 @@ pub enum Error { InvalidStreamKind(u8), #[error(transparent)] InvalidRefName(#[from] fmt::Error), + #[error(transparent)] + InvalidAlias(#[from] node::AliasError), #[error("invalid control message with type `{0}`")] InvalidControlMessage(u8), #[error("invalid protocol version header `{0:x?}`")] @@ -219,6 +223,12 @@ impl Encode for Refs { } } +impl Encode for Alias { + fn encode(&self, writer: &mut W) -> Result { + self.as_ref().encode(writer) + } +} + impl Encode for (A, B) where A: Encode, @@ -284,6 +294,12 @@ impl Decode for git::RefString { } } +impl Decode for Alias { + fn decode(reader: &mut R) -> Result { + String::decode(reader).and_then(|s| Alias::from_str(&s).map_err(Error::from)) + } +} + impl Decode for (A, B) where A: Decode, @@ -573,6 +589,14 @@ mod tests { ); } + #[test] + fn test_alias() { + assert_eq!( + serialize(&Alias::from_str("hello").unwrap()), + vec![5, b'h', b'e', b'l', b'l', b'o'] + ); + } + #[test] fn test_filter_invalid() { let b = bloomy::BloomFilter::with_size(filter::FILTER_SIZE_M / 3); diff --git a/radicle-term/src/io.rs b/radicle-term/src/io.rs index 538a39e4..a8fce6f2 100644 --- a/radicle-term/src/io.rs +++ b/radicle-term/src/io.rs @@ -30,6 +30,7 @@ pub static CONFIG: Lazy = Lazy::new(|| RenderConfig { answer: StyleSheet::new(), highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow), help_message: StyleSheet::new().with_fg(Color::DarkGrey), + default_value: StyleSheet::new().with_fg(Color::LightBlue), error_message: ErrorMessageRenderConfig::default_colored() .with_prefix(Styled::new("โœ—").with_fg(Color::LightRed)), ..RenderConfig::default_colored() @@ -173,16 +174,18 @@ pub fn abort(prompt: D) -> bool { ask(prompt, false) } -pub fn input(message: &str, default: Option) -> anyhow::Result +pub fn input(message: &str, default: Option, help: Option<&str>) -> anyhow::Result where S: fmt::Display + std::str::FromStr + Clone, E: fmt::Debug + fmt::Display, { - let input = CustomType::::new(message).with_render_config(*CONFIG); - let value = match default { - Some(default) => input.with_default(default).prompt()?, - None => input.prompt()?, - }; + let mut input = CustomType::::new(message).with_render_config(*CONFIG); + + input.default = default; + input.help_message = help; + + let value = input.prompt()?; + Ok(value) } diff --git a/radicle-tools/Cargo.toml b/radicle-tools/Cargo.toml index 23aaa0ba..1ca43571 100644 --- a/radicle-tools/Cargo.toml +++ b/radicle-tools/Cargo.toml @@ -23,10 +23,6 @@ path = "../radicle-cli" name = "rad-init" path = "src/rad-init.rs" -[[bin]] -name = "rad-auth" -path = "src/rad-auth.rs" - [[bin]] name = "rad-self" path = "src/rad-self.rs" diff --git a/radicle-tools/src/rad-auth.rs b/radicle-tools/src/rad-auth.rs deleted file mode 100644 index 32d6d600..00000000 --- a/radicle-tools/src/rad-auth.rs +++ /dev/null @@ -1,15 +0,0 @@ -use radicle::profile; -use radicle::profile::{Error, Profile}; - -fn main() -> anyhow::Result<()> { - let profile = match Profile::load() { - Ok(profile) => profile, - Err(Error::NotFound(_)) => Profile::init(profile::home()?, "radicle".to_owned())?, - Err(err) => anyhow::bail!(err), - }; - - println!("id: {}", profile.id()); - println!("home: {}", profile.home().display()); - - Ok(()) -} diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs index aaa98143..f1c62423 100644 --- a/radicle/src/lib.rs +++ b/radicle/src/lib.rs @@ -31,7 +31,7 @@ pub mod prelude { pub use crypto::{PublicKey, Signer, Verified}; pub use identity::{project::Project, Did, Doc, Id}; - pub use node::{NodeId, Timestamp}; + pub use node::{Alias, NodeId, Timestamp}; pub use profile::Profile; pub use storage::{BranchName, ReadRepository, ReadStorage, WriteRepository, WriteStorage}; } diff --git a/radicle/src/node.rs b/radicle/src/node.rs index c6c60946..d91636ba 100644 --- a/radicle/src/node.rs +++ b/radicle/src/node.rs @@ -1,6 +1,7 @@ mod features; pub mod address; +pub mod config; pub mod events; pub mod routing; pub mod tracking; @@ -10,6 +11,7 @@ use std::io::{BufRead, BufReader}; use std::ops::Deref; use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::{fmt, io, net, thread, time}; use amplify::WrapperMut; @@ -23,6 +25,7 @@ use crate::crypto::PublicKey; use crate::identity::Id; use crate::storage::RefUpdate; +pub use config::Config; pub use cyphernet::addr::PeerAddr; pub use events::{Event, Events}; pub use features::Features; @@ -33,6 +36,8 @@ pub const DEFAULT_SOCKET_NAME: &str = "control.sock"; pub const DEFAULT_PORT: u16 = 8776; /// Default timeout when waiting for the node to respond with data. pub const DEFAULT_TIMEOUT: time::Duration = time::Duration::from_secs(9); +/// Maximum length in bytes of a node alias. +pub const MAX_ALIAS_LENGTH: usize = 32; /// Filename of routing table database under the node directory. pub const ROUTING_DB_FILE: &str = "routing.db"; /// Filename of address database under the node directory. @@ -107,6 +112,90 @@ impl fmt::Display for State { } } +/// Node alias. +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct Alias(String); + +impl Alias { + /// Create a new alias from a string. Panics if the string is not a valid alias. + pub fn new(alias: impl ToString) -> Self { + let alias = alias.to_string(); + + match Self::from_str(&alias) { + Ok(a) => a, + Err(e) => panic!("Alias::new: {e}"), + } + } +} + +impl From for String { + fn from(value: Alias) -> Self { + value.0 + } +} + +impl From<&NodeId> for Alias { + fn from(nid: &NodeId) -> Self { + Alias(nid.to_string()) + } +} + +impl fmt::Display for Alias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Deref for Alias { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for Alias { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl From<&Alias> for [u8; 32] { + fn from(input: &Alias) -> [u8; 32] { + let mut alias = [0u8; 32]; + + alias[..input.len()].copy_from_slice(input.as_bytes()); + alias + } +} + +#[derive(thiserror::Error, Debug)] +pub enum AliasError { + #[error("alias cannot be empty")] + Empty, + #[error("alias cannot be greater than {MAX_ALIAS_LENGTH} bytes")] + MaxBytesExceeded, + #[error("alias cannot contain whitespace or control characters")] + InvalidCharacter, +} + +impl FromStr for Alias { + type Err = AliasError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(AliasError::Empty); + } + if s.chars().any(|c| c.is_control() || c.is_whitespace()) { + return Err(AliasError::InvalidCharacter); + } + if s.len() > MAX_ALIAS_LENGTH { + return Err(AliasError::MaxBytesExceeded); + } + Ok(Self(s.to_owned())) + } +} + /// Result of a command, on the node control socket. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "status")] @@ -499,7 +588,7 @@ pub trait Handle: Clone + Sync + Send { /// tracked. fn track_repo(&mut self, id: Id, scope: tracking::Scope) -> Result; /// Start tracking the given node. - fn track_node(&mut self, id: NodeId, alias: Option) -> Result; + fn track_node(&mut self, id: NodeId, alias: Option) -> Result; /// Untrack the given project and delete it from storage. fn untrack_repo(&mut self, id: Id) -> Result; /// Untrack the given node. @@ -671,7 +760,7 @@ impl Handle for Node { Ok(result) } - fn track_node(&mut self, id: NodeId, alias: Option) -> Result { + fn track_node(&mut self, id: NodeId, alias: Option) -> Result { let id = id.to_human(); let args = if let Some(alias) = alias.as_deref() { vec![id.as_str(), alias] @@ -787,23 +876,23 @@ impl Handle for Node { /// A trait for different sources which can potentially return an alias. pub trait AliasStore { /// Returns alias of a `NodeId`. - fn alias(&self, nid: &NodeId) -> Option; + fn alias(&self, nid: &NodeId) -> Option; } impl AliasStore for &T { - fn alias(&self, nid: &NodeId) -> Option { + fn alias(&self, nid: &NodeId) -> Option { (*self).alias(nid) } } impl AliasStore for Box { - fn alias(&self, nid: &NodeId) -> Option { + fn alias(&self, nid: &NodeId) -> Option { self.deref().alias(nid) } } -impl AliasStore for HashMap { - fn alias(&self, nid: &NodeId) -> Option { +impl AliasStore for HashMap { + fn alias(&self, nid: &NodeId) -> Option { self.get(nid).map(ToOwned::to_owned) } } @@ -816,4 +905,19 @@ mod test { fn test_command_name_display() { assert_eq!(CommandName::TrackNode.to_string(), "track-node"); } + + #[test] + fn test_alias() { + assert!(Alias::from_str("cloudhead").is_ok()); + assert!(Alias::from_str("cloud-head").is_ok()); + assert!(Alias::from_str("cl0ud.h3ad$__").is_ok()); + assert!(Alias::from_str("ยฉloudhรจรขd").is_ok()); + + assert!(Alias::from_str("").is_err()); + assert!(Alias::from_str(" ").is_err()); + assert!(Alias::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err()); + assert!(Alias::from_str("cloud\0head").is_err()); + assert!(Alias::from_str("cloud head").is_err()); + assert!(Alias::from_str("cloudhead\n").is_err()); + } } diff --git a/radicle/src/node/address/schema.sql b/radicle/src/node/address/schema.sql index 7d91f41f..e9d1a352 100644 --- a/radicle/src/node/address/schema.sql +++ b/radicle/src/node/address/schema.sql @@ -7,7 +7,7 @@ create table if not exists "nodes" ( -- Node features. "features" integer not null, -- Node alias. - "alias" text default null, + "alias" text not null, --- Node announcement proof-of-work. "pow" integer default 0, -- Node announcement timestamp. diff --git a/radicle/src/node/address/store.rs b/radicle/src/node/address/store.rs index acaee4cc..aff427dd 100644 --- a/radicle/src/node/address/store.rs +++ b/radicle/src/node/address/store.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::str::FromStr; use std::{fmt, io}; use localtime::LocalTime; @@ -7,9 +8,7 @@ use thiserror::Error; use crate::node; use crate::node::address::{KnownAddress, Source}; -use crate::node::Address; -use crate::node::AliasStore; -use crate::node::NodeId; +use crate::node::{Address, Alias, AliasError, AliasStore, NodeId}; use crate::prelude::Timestamp; use crate::sql::transaction; @@ -21,6 +20,8 @@ pub enum Error { /// I/O error. #[error("i/o error: {0}")] Io(#[from] io::Error), + #[error("alias error: {0}")] + InvalidAlias(#[from] AliasError), /// An Internal error. #[error("internal error: {0}")] Internal(#[from] sql::Error), @@ -77,7 +78,7 @@ impl Store for Book { if let Some(Ok(row)) = stmt.into_iter().next() { let features = row.read::("features"); - let alias = row.read::, _>("alias").map(ToOwned::to_owned); + let alias = Alias::from_str(row.read::<&str, _>("alias"))?; let timestamp = row.read::("timestamp") as Timestamp; let pow = row.read::("pow") as u32; let mut addrs = Vec::new(); @@ -130,7 +131,7 @@ impl Store for Book { &mut self, node: &NodeId, features: node::Features, - alias: &str, + alias: Alias, pow: u32, timestamp: Timestamp, addrs: impl IntoIterator, @@ -143,15 +144,10 @@ impl Store for Book { SET features = ?2, alias = ?3, pow = ?4, timestamp = ?5 WHERE timestamp < ?5", )?; - let alias = if alias.is_empty() { - sql::Value::Null - } else { - sql::Value::String(alias.to_owned()) - }; stmt.bind((1, node))?; stmt.bind((2, features))?; - stmt.bind((3, alias))?; + stmt.bind((3, sql::Value::String(alias.into())))?; stmt.bind((4, pow as i64))?; stmt.bind((5, timestamp as i64))?; stmt.next()?; @@ -263,9 +259,9 @@ impl Store for Book { impl AliasStore for Book { /// Retrieve `alias` of given node. /// Calls `Self::get` under the hood. - fn alias(&self, nid: &NodeId) -> Option { + fn alias(&self, nid: &NodeId) -> Option { self.get(nid) - .map(|node| node.and_then(|n| n.alias)) + .map(|node| node.map(|n| n.alias)) .unwrap_or(None) } } @@ -283,7 +279,7 @@ pub trait Store { &mut self, node: &NodeId, features: node::Features, - alias: &str, + alias: Alias, pow: u32, timestamp: Timestamp, addrs: impl IntoIterator, @@ -409,16 +405,16 @@ mod test { let timestamp = LocalTime::now().as_millis(); cache - .insert(&alice, features, "alice", 16, timestamp, []) + .insert(&alice, features, Alias::new("alice"), 16, timestamp, []) .unwrap(); let node = cache.get(&alice).unwrap().unwrap(); - assert_eq!(node.alias.as_deref(), Some("alice")); + assert_eq!(node.alias.as_ref(), "alice"); cache - .insert(&alice, features, "", 16, timestamp + 1, []) + .insert(&alice, features, Alias::new("bob"), 16, timestamp + 1, []) .unwrap(); let node = cache.get(&alice).unwrap().unwrap(); - assert_eq!(node.alias.as_deref(), None); + assert_eq!(node.alias.as_ref(), "bob"); } #[test] @@ -435,7 +431,14 @@ mod test { last_attempt: None, }; let inserted = cache - .insert(&alice, features, "alice", 16, timestamp, [ka.clone()]) + .insert( + &alice, + features, + Alias::new("alice"), + 16, + timestamp, + [ka.clone()], + ) .unwrap(); assert!(inserted); @@ -444,7 +447,7 @@ mod test { assert_eq!(node.features, features); assert_eq!(node.pow, 16); assert_eq!(node.timestamp, timestamp); - assert_eq!(node.alias.as_deref(), Some("alice")); + assert_eq!(node.alias.as_ref(), "alice"); assert_eq!(node.addrs, vec![ka]); } @@ -454,6 +457,7 @@ mod test { let mut cache = Book::memory().unwrap(); let features = node::Features::SEED; let timestamp = LocalTime::now().as_millis(); + let alias = Alias::new("alice"); let ka = KnownAddress { addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(), @@ -462,12 +466,12 @@ mod test { last_attempt: None, }; let inserted = cache - .insert(&alice, features, "alice", 0, timestamp, [ka.clone()]) + .insert(&alice, features, alias.clone(), 0, timestamp, [ka.clone()]) .unwrap(); assert!(inserted); let inserted = cache - .insert(&alice, features, "alice", 0, timestamp, [ka]) + .insert(&alice, features, alias, 0, timestamp, [ka]) .unwrap(); assert!(!inserted); @@ -480,6 +484,8 @@ mod test { let mut cache = Book::memory().unwrap(); let timestamp = LocalTime::now().as_millis(); let features = node::Features::SEED; + let alias1 = Alias::new("alice"); + let alias2 = Alias::new("~alice~"); let ka = KnownAddress { addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(), source: Source::Peer, @@ -488,45 +494,38 @@ mod test { }; let updated = cache - .insert(&alice, features, "alice", 0, timestamp, [ka.clone()]) + .insert(&alice, features, alias1, 0, timestamp, [ka.clone()]) .unwrap(); assert!(updated); let updated = cache - .insert(&alice, features, "~alice~", 0, timestamp, []) + .insert(&alice, features, alias2.clone(), 0, timestamp, []) .unwrap(); assert!(!updated, "Can't update using the same timestamp"); let updated = cache - .insert(&alice, features, "~alice~", 0, timestamp - 1, []) + .insert(&alice, features, alias2.clone(), 0, timestamp - 1, []) .unwrap(); assert!(!updated, "Can't update using a smaller timestamp"); let node = cache.get(&alice).unwrap().unwrap(); - assert_eq!(node.alias.as_deref(), Some("alice")); + assert_eq!(node.alias.as_ref(), "alice"); assert_eq!(node.timestamp, timestamp); assert_eq!(node.pow, 0); let updated = cache - .insert(&alice, features, "~alice~", 0, timestamp + 1, []) + .insert(&alice, features, alias2.clone(), 0, timestamp + 1, []) .unwrap(); assert!(updated, "Can update with a larger timestamp"); let updated = cache - .insert( - &alice, - node::Features::NONE, - "~alice~", - 1, - timestamp + 2, - [], - ) + .insert(&alice, node::Features::NONE, alias2, 1, timestamp + 2, []) .unwrap(); assert!(updated); let node = cache.get(&alice).unwrap().unwrap(); assert_eq!(node.features, node::Features::NONE); - assert_eq!(node.alias.as_deref(), Some("~alice~")); + assert_eq!(node.alias.as_ref(), "~alice~"); assert_eq!(node.timestamp, timestamp + 2); assert_eq!(node.pow, 1); assert_eq!(node.addrs, vec![ka]); @@ -539,6 +538,8 @@ mod test { let mut cache = Book::memory().unwrap(); let timestamp = LocalTime::now().as_millis(); let features = node::Features::SEED; + let alice_alias = Alias::new("alice"); + let bob_alias = Alias::new("bob"); for addr in [ ([4, 4, 4, 4], 8776), @@ -552,10 +553,17 @@ mod test { last_attempt: None, }; cache - .insert(&alice, features, "alice", 0, timestamp, [ka.clone()]) + .insert( + &alice, + features, + alice_alias.clone(), + 0, + timestamp, + [ka.clone()], + ) .unwrap(); cache - .insert(&bob, features, "bob", 0, timestamp, [ka]) + .insert(&bob, features, bob_alias.clone(), 0, timestamp, [ka]) .unwrap(); } assert_eq!(cache.len().unwrap(), 6); @@ -577,6 +585,7 @@ mod test { let mut expected = Vec::new(); let timestamp = LocalTime::now().as_millis(); let features = node::Features::SEED; + let alias = Alias::new("alice"); for id in ids { let ip = rng.u32(..); @@ -590,7 +599,7 @@ mod test { }; expected.push((id, ka.clone())); cache - .insert(&id, features, "alias", 0, timestamp, [ka]) + .insert(&id, features, alias.clone(), 0, timestamp, [ka]) .unwrap(); } diff --git a/radicle/src/node/address/types.rs b/radicle/src/node/address/types.rs index ac1c09c7..53b1b700 100644 --- a/radicle/src/node/address/types.rs +++ b/radicle/src/node/address/types.rs @@ -5,7 +5,7 @@ use nonempty::NonEmpty; use crate::collections::HashMap; use crate::node; -use crate::node::Address; +use crate::node::{Address, Alias}; use crate::prelude::Timestamp; /// A map with the ability to randomly select values. @@ -78,7 +78,7 @@ impl DerefMut for AddressBook { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Node { /// Advertized alias. - pub alias: Option, + pub alias: Alias, /// Advertized features. pub features: node::Features, /// Advertized addresses diff --git a/radicle/src/node/config.rs b/radicle/src/node/config.rs new file mode 100644 index 00000000..c57cfcd7 --- /dev/null +++ b/radicle/src/node/config.rs @@ -0,0 +1,126 @@ +use std::ops::Deref; + +use cyphernet::addr::PeerAddr; +use localtime::LocalDuration; + +use crate::node; +use crate::node::tracking::{Policy, Scope}; +use crate::node::{Address, Alias, NodeId}; + +/// Peer-to-peer network. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Network { + #[default] + Main, + Test, +} + +/// Configuration parameters defining attributes of minima and maxima. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Limits { + /// Number of routing table entries before we start pruning. + pub routing_max_size: usize, + /// How long to keep a routing table entry before being pruned. + #[serde(with = "crate::serde_ext::localtime::duration")] + pub routing_max_age: LocalDuration, + /// Maximum number of concurrent fetches per per connection. + pub fetch_concurrency: usize, +} + +impl Default for Limits { + fn default() -> Self { + Self { + routing_max_size: 1000, + routing_max_age: LocalDuration::from_mins(7 * 24 * 60), + fetch_concurrency: 1, + } + } +} + +/// Full address used to connect to a remote node. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +pub struct ConnectAddress(#[serde(with = "crate::serde_ext::string")] PeerAddr); + +impl From> for ConnectAddress { + fn from(value: PeerAddr) -> Self { + Self(value) + } +} + +impl From for (NodeId, Address) { + fn from(value: ConnectAddress) -> Self { + (value.0.id, value.0.addr) + } +} + +impl From<(NodeId, Address)> for ConnectAddress { + fn from((id, addr): (NodeId, Address)) -> Self { + Self(PeerAddr { id, addr }) + } +} + +impl Deref for ConnectAddress { + type Target = PeerAddr; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Service configuration. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Config { + /// Node alias. + pub alias: Alias, + /// Peers to connect to on startup. + /// Connections to these peers will be maintained. + pub connect: Vec, + /// Specify the node's public addresses + pub external_addresses: Vec
, + /// Peer-to-peer network. + pub network: Network, + /// Whether or not our node should relay inventories. + pub relay: bool, + /// Configured service limits. + pub limits: Limits, + /// Default tracking policy. + pub policy: Policy, + /// Default tracking scope. + pub scope: Scope, +} + +impl Config { + pub fn new(alias: Alias) -> Self { + Self { + alias, + connect: Vec::default(), + external_addresses: vec![], + network: Network::default(), + relay: true, + limits: Limits::default(), + policy: Policy::default(), + scope: Scope::default(), + } + } +} + +impl Config { + pub fn peer(&self, id: &NodeId) -> Option<&Address> { + self.connect + .iter() + .find(|ca| &ca.id == id) + .map(|ca| &ca.addr) + } + + pub fn is_persistent(&self, id: &NodeId) -> bool { + self.peer(id).is_some() + } + + pub fn features(&self) -> node::Features { + node::Features::SEED + } +} diff --git a/radicle/src/node/tracking.rs b/radicle/src/node/tracking.rs index cf708642..94a44349 100644 --- a/radicle/src/node/tracking.rs +++ b/radicle/src/node/tracking.rs @@ -8,7 +8,7 @@ use thiserror::Error; use crate::prelude::Id; -use super::NodeId; +pub use super::{Alias, NodeId}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Repo { @@ -24,11 +24,9 @@ pub struct Node { pub policy: Policy, } -/// Node alias. -pub type Alias = String; - /// Tracking policy. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum Policy { /// The resource is tracked. Track, @@ -91,6 +89,7 @@ impl TryFrom<&sqlite::Value> for Policy { /// Tracking scope of a repository tracking policy. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum Scope { /// Track remotes of nodes that are already tracked. #[default] diff --git a/radicle/src/node/tracking/store.rs b/radicle/src/node/tracking/store.rs index 6053e270..f21d3029 100644 --- a/radicle/src/node/tracking/store.rs +++ b/radicle/src/node/tracking/store.rs @@ -1,11 +1,11 @@ #![allow(clippy::type_complexity)] use std::path::Path; -use std::{fmt, io, ops::Not as _, time}; +use std::{fmt, io, ops::Not as _, str::FromStr, time}; use sqlite as sql; use thiserror::Error; -use crate::node::AliasStore; +use crate::node::{Alias, AliasStore}; use crate::prelude::{Id, NodeId}; use super::{Node, Policy, Repo, Scope}; @@ -188,7 +188,11 @@ impl Config { if let Some(Ok(row)) = stmt.into_iter().next() { let alias = row.read::<&str, _>("alias"); - let alias = alias.is_empty().not().then_some(alias.to_owned()); + let alias = alias + .is_empty() + .not() + .then_some(alias.to_owned()) + .and_then(|s| Alias::from_str(&s).ok()); let policy = row.read::("policy"); return Ok(Some(Node { @@ -229,7 +233,11 @@ impl Config { while let Some(Ok(row)) = stmt.next() { let id = row.read("id"); let alias = row.read::<&str, _>("alias").to_owned(); - let alias = alias.is_empty().not().then_some(alias.to_owned()); + let alias = alias + .is_empty() + .not() + .then_some(alias.to_owned()) + .and_then(|s| Alias::from_str(&s).ok()); let policy = row.read::("policy"); entries.push(Node { id, alias, policy }); @@ -260,7 +268,7 @@ impl Config { impl AliasStore for Config { /// Retrieve `alias` of given node. /// Calls `Self::node_policy` under the hood. - fn alias(&self, nid: &NodeId) -> Option { + fn alias(&self, nid: &NodeId) -> Option { self.node_policy(nid) .map(|node| node.and_then(|n| n.alias)) .unwrap_or(None) @@ -334,7 +342,7 @@ mod test { assert!(db.track_node(&id, Some("eve")).unwrap()); assert_eq!( db.node_policy(&id).unwrap().unwrap().alias, - Some(String::from("eve")) + Some(Alias::from_str("eve").unwrap()) ); assert!(db.track_node(&id, None).unwrap()); assert_eq!(db.node_policy(&id).unwrap().unwrap().alias, None); @@ -342,7 +350,7 @@ mod test { assert!(db.track_node(&id, Some("alice")).unwrap()); assert_eq!( db.node_policy(&id).unwrap().unwrap().alias, - Some(String::from("alice")) + Some(Alias::new("alice")) ); } diff --git a/radicle/src/profile.rs b/radicle/src/profile.rs index ef23123b..9ff9c632 100644 --- a/radicle/src/profile.rs +++ b/radicle/src/profile.rs @@ -10,16 +10,18 @@ //! node/ //! control.sock # Node control socket //! +use std::io::Write; use std::path::{Path, PathBuf}; -use std::{fs, io}; +use std::{fs, io, str::FromStr}; +use serde::Serialize; use thiserror::Error; use crate::crypto::ssh::agent::Agent; use crate::crypto::ssh::{keystore, Keystore, Passphrase}; use crate::crypto::{PublicKey, Signer}; use crate::node; -use crate::node::{address, routing, tracking, AliasStore}; +use crate::node::{address, routing, tracking, Alias, AliasStore}; use crate::prelude::Did; use crate::prelude::NodeId; use crate::storage::git::transport; @@ -49,10 +51,12 @@ pub enum Error { #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] + Config(#[from] ConfigError), + #[error(transparent)] Keystore(#[from] keystore::Error), #[error(transparent)] MemorySigner(#[from] keystore::MemorySignerError), - #[error("no profile found at the filepath '{0}'")] + #[error("no profile found at path '{0}'")] NotFound(PathBuf), #[error("error connecting to ssh-agent: {0}")] Agent(#[from] crate::crypto::ssh::agent::Error), @@ -64,19 +68,86 @@ pub enum Error { AddressStore(#[from] node::address::Error), } +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("failed to load node configuration from {0}: {1}")] + Io(PathBuf, io::Error), + #[error("failed to decode node configuration from {0}: {1}")] + Json(PathBuf, serde_json::Error), +} + +/// Local radicle configuration. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Config { + pub node: node::Config, +} + +impl Config { + /// Initialize a new configuration. Fails if the path already exists. + pub fn init(alias: Alias, path: &Path) -> io::Result { + let cfg = Self { + node: node::Config::new(alias), + }; + let mut file = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(path)?; + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut serializer = serde_json::Serializer::with_formatter(&file, formatter); + + cfg.serialize(&mut serializer)?; + file.write_all(b"\n")?; + file.sync_all()?; + + Ok(cfg) + } + + /// Load a configuration from the given path. + pub fn load(path: &Path) -> Result { + match fs::File::open(path) { + Ok(cfg) => { + serde_json::from_reader(cfg).map_err(|e| ConfigError::Json(path.to_path_buf(), e)) + } + Err(e) => { + let Ok(user) = env::var("USER") else { + return Err(ConfigError::Io(path.to_owned(), e)); + }; + let Ok(alias) = Alias::from_str(&user) else { + return Err(ConfigError::Io(path.to_owned(), e)); + }; + Ok(Config { + node: node::Config::new(alias), + }) + } + } + } + + /// Get the user alias. + pub fn alias(&self) -> &Alias { + &self.node.alias + } +} + #[derive(Debug, Clone)] pub struct Profile { pub home: Home, pub storage: Storage, pub keystore: Keystore, pub public_key: PublicKey, + pub config: Config, } impl Profile { - pub fn init(home: Home, passphrase: impl Into) -> Result { + pub fn init( + home: Home, + alias: Alias, + passphrase: impl Into, + ) -> Result { let storage = Storage::open(home.storage())?; let keystore = Keystore::new(&home.keys()); let public_key = keystore.init("radicle", passphrase)?; + let config = Config::init(alias, home.config().as_path())?; transport::local::register(storage.clone()); @@ -85,6 +156,7 @@ impl Profile { storage, keystore, public_key, + config, }) } @@ -95,6 +167,7 @@ impl Profile { let public_key = keystore .public_key()? .ok_or_else(|| Error::NotFound(home.path().to_path_buf()))?; + let config = Config::load(home.config().as_path())?; transport::local::register(storage.clone()); @@ -103,6 +176,7 @@ impl Profile { storage, keystore, public_key, + config, }) } @@ -194,7 +268,7 @@ pub struct Aliases { impl AliasStore for Aliases { /// Retrieve `alias` of given node. /// First looks in `tracking.db` and then `addresses.db`. - fn alias(&self, nid: &NodeId) -> Option { + fn alias(&self, nid: &NodeId) -> Option { self.tracking .alias(nid) .or_else(|| self.addresses.alias(nid)) @@ -267,6 +341,10 @@ impl Home { self.path.join("storage") } + pub fn config(&self) -> PathBuf { + self.path.join("config.json") + } + pub fn keys(&self) -> PathBuf { self.path.join("keys") } diff --git a/radicle/src/serde_ext.rs b/radicle/src/serde_ext.rs index cc8e5eb2..4157c23e 100644 --- a/radicle/src/serde_ext.rs +++ b/radicle/src/serde_ext.rs @@ -24,28 +24,53 @@ pub mod string { } } -/// Unlike the default `serde` instance for `LocalTime`, this encodes and decodes using seconds +/// Unlike the default `serde` instances from `localtime`, this encodes and decodes using seconds /// instead of milliseconds. pub mod localtime { - use localtime::LocalTime; - use serde::{de, Deserialize, Deserializer, Serializer}; + pub mod time { + use localtime::LocalTime; + use serde::{de, Deserialize, Deserializer, Serializer}; - pub fn serialize(value: &LocalTime, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&value.as_secs()) + pub fn serialize(value: &LocalTime, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(&value.as_secs()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let seconds: u64 = String::deserialize(deserializer)? + .parse() + .map_err(de::Error::custom)?; + + Ok(LocalTime::from_secs(seconds)) + } } - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let seconds: u64 = String::deserialize(deserializer)? - .parse() - .map_err(de::Error::custom)?; + pub mod duration { + use localtime::LocalDuration; + use serde::{de, Deserialize, Deserializer, Serializer}; - Ok(LocalTime::from_secs(seconds)) + pub fn serialize(value: &LocalDuration, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(&value.as_secs()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let seconds: u64 = String::deserialize(deserializer)? + .parse() + .map_err(de::Error::custom)?; + + Ok(LocalDuration::from_secs(seconds)) + } } } diff --git a/radicle/src/test/arbitrary.rs b/radicle/src/test/arbitrary.rs index c8f65905..54d64fc5 100644 --- a/radicle/src/test/arbitrary.rs +++ b/radicle/src/test/arbitrary.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashSet}; use std::hash::Hash; use std::ops::RangeBounds; +use std::str::FromStr; use std::{iter, net}; use crypto::test::signer::MockSigner; @@ -14,7 +15,7 @@ use crate::identity::{ project::Project, Did, }; -use crate::node::Address; +use crate::node::{Address, Alias}; use crate::storage; use crate::storage::refs::{Refs, SignedRefs}; use crate::test::storage::{MockRepository, MockStorage}; @@ -227,3 +228,13 @@ impl Arbitrary for Address { }) } } + +impl Arbitrary for Alias { + fn arbitrary(g: &mut qcheck::Gen) -> Self { + let s = g + .choose(&["cloudhead", "alice", "bob", "john-lu", "f0_"]) + .unwrap(); + + Alias::from_str(s).unwrap() + } +}