cli: Update commands for private repos

Several commands are updated to better support private repos.
This commit is contained in:
cloudhead 2023-08-29 15:49:07 +02:00
parent 257b950d6f
commit 9a54c8da7b
No known key found for this signature in database
6 changed files with 178 additions and 54 deletions

View File

@ -1,10 +1,11 @@
use std::{ffi::OsString, str::FromStr as _}; use std::io::IsTerminal;
use std::{ffi::OsString, io, str::FromStr as _};
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use radicle::cob::identity::{self, Proposal, Proposals, Revision, RevisionId}; use radicle::cob::identity::{self, Proposal, Proposals, Revision, RevisionId};
use radicle::git::Oid; use radicle::git::Oid;
use radicle::identity::Identity; use radicle::identity::{Identity, Visibility};
use radicle::prelude::{Did, Doc}; use radicle::prelude::{Did, Doc};
use radicle::storage::ReadStorage as _; use radicle::storage::ReadStorage as _;
use radicle_crypto::Verified; use radicle_crypto::Verified;
@ -24,14 +25,16 @@ Usage
rad id (update|edit) [--title|-t] [--description|-d] rad id (update|edit) [--title|-t] [--description|-d]
[--delegates <did>] [--threshold <num>] [--delegates <did>] [--threshold <num>]
[--no-confirm] [<option>...] [--visibility <private | public>]
[--allow <did>] [--no-confirm] [<option>...]
rad id list [<option>...] rad id list [<option>...]
rad id rebase <id> [--rev <revision-id>] [<option>...] rad id rebase <id> [--rev <revision-id>] [<option>...]
rad id show <id> [--rev <revision-id>] [--revisions] [<option>...] rad id show <id> [--rev <revision-id>] [--revisions] [<option>...]
rad id (accept|reject|close|commit) [--rev <revision-id>] [--no-confirm] [<option>...] rad id (accept|reject|close|commit) <id> [--rev <revision-id>] [--no-confirm] [<option>...]
Options Options
--quiet, -q Don't print anything
--help Print help --help Print help
"#, "#,
}; };
@ -67,6 +70,7 @@ pub enum Operation {
title: Option<String>, title: Option<String>,
description: Option<String>, description: Option<String>,
delegates: Vec<Did>, delegates: Vec<Did>,
visibility: Option<Visibility>,
threshold: Option<usize>, threshold: Option<usize>,
}, },
Update { Update {
@ -114,6 +118,7 @@ pub enum OperationName {
pub struct Options { pub struct Options {
pub op: Operation, pub op: Operation,
pub interactive: Interactive, pub interactive: Interactive,
pub quiet: bool,
} }
impl Args for Options { impl Args for Options {
@ -127,9 +132,11 @@ impl Args for Options {
let mut title: Option<String> = None; let mut title: Option<String> = None;
let mut description: Option<String> = None; let mut description: Option<String> = None;
let mut delegates: Vec<Did> = Vec::new(); let mut delegates: Vec<Did> = Vec::new();
let mut visibility: Option<Visibility> = None;
let mut threshold: Option<usize> = None; let mut threshold: Option<usize> = None;
let mut interactive = Interactive::Yes; let mut interactive = Interactive::from(io::stdout().is_terminal());
let mut show_revisions = false; let mut show_revisions = false;
let mut quiet = false;
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
@ -142,6 +149,9 @@ impl Args for Options {
Long("description") if op == Some(OperationName::Edit) => { Long("description") if op == Some(OperationName::Edit) => {
description = Some(parser.value()?.to_string_lossy().into()); description = Some(parser.value()?.to_string_lossy().into());
} }
Long("quiet") | Short('q') => {
quiet = true;
}
Long("no-confirm") => { Long("no-confirm") => {
interactive = Interactive::No; interactive = Interactive::No;
} }
@ -169,6 +179,21 @@ impl Args for Options {
let did = term::args::did(&parser.value()?)?; let did = term::args::did(&parser.value()?)?;
delegates.push(did); delegates.push(did);
} }
Long("allow") => {
let value = parser.value()?;
let did = term::args::did(&value)?;
if let Some(Visibility::Private { allow }) = &mut visibility {
allow.insert(did);
} else {
visibility = Some(Visibility::private([did]));
}
}
Long("visibility") => {
let value = parser.value()?;
let value = term::args::parse_value("visibility", value)?;
visibility = Some(value);
}
Long("threshold") => { Long("threshold") => {
threshold = Some(parser.value()?.to_string_lossy().parse()?); threshold = Some(parser.value()?.to_string_lossy().parse()?);
} }
@ -198,6 +223,7 @@ impl Args for Options {
title, title,
description, description,
delegates, delegates,
visibility,
threshold, threshold,
}, },
OperationName::Update => Operation::Update { OperationName::Update => Operation::Update {
@ -226,7 +252,14 @@ impl Args for Options {
id: id.ok_or_else(|| anyhow!("a proposal must be provided"))?, id: id.ok_or_else(|| anyhow!("a proposal must be provided"))?,
}, },
}; };
Ok((Options { op, interactive }, vec![])) Ok((
Options {
op,
interactive,
quiet,
},
vec![],
))
} }
} }
@ -247,11 +280,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let (rid, revision) = select(&proposal, rev, &previous, interactive)?; let (rid, revision) = select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous); warn_out_of_date(revision, &previous);
let yes = confirm(interactive, "Are you sure you want to accept?"); let yes = confirm(interactive, "Are you sure you want to accept?");
if yes { if yes {
let (_, signature) = revision.proposed.sign(&signer)?; let (_, signature) = revision.proposed.sign(&signer)?;
proposal.accept(rid, signature, &signer)?; proposal.accept(rid, signature, &signer)?;
term::success!("Accepted proposal ✓");
print(&proposal, &previous, None)?; if !options.quiet {
term::success!("Accepted proposal ✓");
print(&proposal, &previous, None)?;
}
} }
} }
Operation::Reject { id, rev } => { Operation::Reject { id, rev } => {
@ -260,22 +297,28 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let (rid, revision) = select(&proposal, rev, &previous, interactive)?; let (rid, revision) = select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous); warn_out_of_date(revision, &previous);
let yes = confirm(interactive, "Are you sure you want to reject?"); let yes = confirm(interactive, "Are you sure you want to reject?");
if yes { if yes {
proposal.reject(rid, &signer)?; proposal.reject(rid, &signer)?;
term::success!("Rejected proposal 👎");
print(&proposal, &previous, None)?; if !options.quiet {
term::success!("Rejected proposal 👎");
print(&proposal, &previous, None)?;
}
} }
} }
Operation::Edit { Operation::Edit {
title, title,
description, description,
delegates, delegates,
visibility,
threshold, threshold,
} => { } => {
let proposed = { let proposed = {
let mut proposed = previous.doc.clone(); let mut proposed = previous.doc.clone();
proposed.threshold = threshold.unwrap_or(proposed.threshold); proposed.threshold = threshold.unwrap_or(proposed.threshold);
proposed.delegates.extend(delegates); proposed.delegates.extend(delegates);
proposed.visibility = visibility.unwrap_or(proposed.visibility);
proposed proposed
}; };
@ -296,11 +339,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
create.proposed, create.proposed,
&signer, &signer,
)?; )?;
term::success!( if options.quiet {
"Identity proposal '{}' created", term::print(proposal.id);
term::format::highlight(proposal.id) } else {
); term::success!(
print(&proposal, &previous, None)?; "Identity proposal '{}' created",
term::format::highlight(proposal.id)
);
print(&proposal, &previous, None)?;
}
} }
Operation::Update { Operation::Update {
id, id,
@ -342,15 +389,20 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if yes { if yes {
proposal.edit(update.title, update.description, &signer)?; proposal.edit(update.title, update.description, &signer)?;
let revision = proposal.update(previous.current, update.proposed, &signer)?; let revision = proposal.update(previous.current, update.proposed, &signer)?;
term::success!(
"Identity proposal '{}' updated", if options.quiet {
term::format::highlight(proposal.id) term::print(revision.to_string());
); } else {
term::success!( term::success!(
"Revision '{}'", "Identity proposal '{}' updated",
term::format::highlight(revision.to_string()) term::format::highlight(proposal.id)
); );
print(&proposal, &previous, None)?; term::success!(
"Revision '{}'",
term::format::highlight(revision.to_string())
);
print(&proposal, &previous, None)?;
}
} }
} }
Operation::Rebase { id, rev } => { Operation::Rebase { id, rev } => {
@ -362,15 +414,20 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if yes { if yes {
let revision = let revision =
proposal.update(previous.current, revision.proposed.clone(), &signer)?; proposal.update(previous.current, revision.proposed.clone(), &signer)?;
term::success!(
"Identity proposal '{}' rebased", if options.quiet {
term::format::highlight(proposal.id) term::print(revision.to_string());
); } else {
term::success!( term::success!(
"Revision '{}'", "Identity proposal '{}' rebased",
term::format::highlight(revision.to_string()) term::format::highlight(proposal.id)
); );
print(&proposal, &previous, None)?; term::success!(
"Revision '{}'",
term::format::highlight(revision.to_string())
);
print(&proposal, &previous, None)?;
}
} }
} }
Operation::List => { Operation::List => {
@ -413,21 +470,31 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let (rid, revision) = commit_select(&proposal, rev, &previous, interactive)?; let (rid, revision) = commit_select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous); warn_out_of_date(revision, &previous);
let yes = confirm(interactive, "Are you sure you want to commit?"); let yes = confirm(interactive, "Are you sure you want to commit?");
if yes { if yes {
let id = Proposal::commit(&proposal, &rid, signer.public_key(), &repo, &signer)?; let id = Proposal::commit(&proposal, &rid, signer.public_key(), &repo, &signer)?;
proposal.commit(&signer)?; proposal.commit(&signer)?;
term::success!("Committed new identity '{}'", id.current);
print(&proposal, &previous, None)?; if options.quiet {
term::print(id.current);
} else {
term::success!("Committed new identity '{}'", id.current);
print(&proposal, &previous, None)?;
}
} }
} }
Operation::Close { id } => { Operation::Close { id } => {
let id = id.resolve(&repo.backend)?; let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?; let mut proposal = proposals.get_mut(&id)?;
let yes = confirm(interactive, "Are you sure you want to close?"); let yes = confirm(interactive, "Are you sure you want to close?");
if yes { if yes {
proposal.close(&signer)?; proposal.close(&signer)?;
term::success!("Closed identity proposal '{}'", id);
print(&proposal, &previous, None)?; if !options.quiet {
term::success!("Closed identity proposal '{}'", id);
print(&proposal, &previous, None)?;
}
} }
} }
Operation::Show { Operation::Show {

View File

@ -182,7 +182,12 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
let interactive = options.interactive; let interactive = options.interactive;
term::headline(format!( term::headline(format!(
"Initializing radicle 👾 project in {}", "Initializing{}radicle 👾 project in {}",
if !options.visibility.is_public() {
term::format::yellow(" private ")
} else {
term::format::default(" ")
},
if path == cwd { if path == cwd {
term::format::highlight(".").to_string() term::format::highlight(".").to_string()
} else { } else {

View File

@ -37,6 +37,7 @@ Options
--id Return the repository identifier (RID) --id Return the repository identifier (RID)
--payload Inspect the repository's identity payload --payload Inspect the repository's identity payload
--refs Inspect the repository's refs on the local device --refs Inspect the repository's refs on the local device
--identity Inspect the identity document
--delegates Inspect the repository's delegates --delegates Inspect the repository's delegates
--policy Inspect the repository's tracking policy --policy Inspect the repository's tracking policy
--history Show the history of the repository identity document --history Show the history of the repository identity document
@ -49,6 +50,7 @@ pub enum Target {
Refs, Refs,
Payload, Payload,
Delegates, Delegates,
Identity,
Policy, Policy,
History, History,
#[default] #[default]
@ -89,6 +91,9 @@ impl Args for Options {
Long("history") => { Long("history") => {
target = Target::History; target = Target::History;
} }
Long("identity") => {
target = Target::Identity;
}
Long("id") => { Long("id") => {
target = Target::Id; target = Target::Id;
} }
@ -147,6 +152,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.payload)?)? colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.payload)?)?
); );
} }
Target::Identity => {
println!(
"{}",
colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.doc)?)?
);
}
Target::Policy => { Target::Policy => {
let tracking = profile.tracking()?; let tracking = profile.tracking()?;
if let Some(repo) = tracking.repo_policy(&id)? { if let Some(repo) = tracking.repo_policy(&id)? {

View File

@ -16,6 +16,8 @@ Usage
Options Options
--private Show only private repositories
--public Show only public repositories
--verbose, -v Verbose output --verbose, -v Verbose output
--help Print help --help Print help
"#, "#,
@ -23,6 +25,8 @@ Options
pub struct Options { pub struct Options {
verbose: bool, verbose: bool,
public: bool,
private: bool,
} }
impl Args for Options { impl Args for Options {
@ -31,18 +35,33 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut verbose = false; let mut verbose = false;
let mut private = false;
let mut public = false;
if let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
match arg { match arg {
Long("help") | Short('h') => { Long("help") | Short('h') => {
return Err(Error::Help.into()); return Err(Error::Help.into());
} }
Long("private") => {
private = true;
}
Long("public") => {
public = true;
}
Long("verbose") | Short('v') => verbose = true, Long("verbose") | Short('v') => verbose = true,
_ => return Err(anyhow::anyhow!(arg.unexpected())), _ => return Err(anyhow::anyhow!(arg.unexpected())),
} }
} }
Ok((Options { verbose }, vec![])) Ok((
Options {
verbose,
private,
public,
},
vec![],
))
} }
} }
@ -52,6 +71,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let mut table = term::Table::default(); let mut table = term::Table::default();
for (id, head, doc) in storage.repositories()? { for (id, head, doc) in storage.repositories()? {
if doc.visibility.is_public() && options.private && !options.public {
continue;
}
if !doc.visibility.is_public() && !options.private && options.public {
continue;
}
let proj = match doc.verified()?.project() { let proj = match doc.verified()?.project() {
Ok(proj) => proj, Ok(proj) => proj,
Err(err) => { Err(err) => {

View File

@ -1,11 +1,12 @@
mod id; mod id;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt; use std::fmt;
use std::fmt::Write as _; use std::fmt::Write as _;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{Deref, Not}; use std::ops::{Deref, Not};
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -144,11 +145,27 @@ pub enum Visibility {
Public, Public,
/// Delegates plus the allowed DIDs. /// Delegates plus the allowed DIDs.
Private { Private {
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
allow: Vec<Did>, allow: BTreeSet<Did>,
}, },
} }
#[derive(Error, Debug)]
#[error("'{0}' is not a valid visibility type")]
pub struct VisibilityParseError(String);
impl FromStr for Visibility {
type Err = VisibilityParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"public" => Ok(Visibility::Public),
"private" => Ok(Visibility::private([])),
_ => Err(VisibilityParseError(s.to_owned())),
}
}
}
impl Visibility { impl Visibility {
/// Check whether the visibility is public. /// Check whether the visibility is public.
pub fn is_public(&self) -> bool { pub fn is_public(&self) -> bool {
@ -156,9 +173,9 @@ impl Visibility {
} }
/// Private visibility with list of allowed DIDs beyond the repository delegates. /// Private visibility with list of allowed DIDs beyond the repository delegates.
pub fn private(allow: impl Into<Vec<Did>>) -> Self { pub fn private(allow: impl IntoIterator<Item = Did>) -> Self {
Self::Private { Self::Private {
allow: allow.into(), allow: BTreeSet::from_iter(allow),
} }
} }
} }
@ -545,16 +562,14 @@ mod test {
serde_json::json!({ "type": "public" }) serde_json::json!({ "type": "public" })
); );
assert_eq!( assert_eq!(
serde_json::to_value(Visibility::Private { allow: vec![] }).unwrap(), serde_json::to_value(Visibility::private([])).unwrap(),
serde_json::json!({ "type": "private" }) serde_json::json!({ "type": "private" })
); );
assert_eq!( assert_eq!(
serde_json::to_value(Visibility::Private { serde_json::to_value(Visibility::private([Did::from_str(
allow: vec![Did::from_str( "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT" )
) .unwrap()]))
.unwrap()]
})
.unwrap(), .unwrap(),
serde_json::json!({ "type": "private", "allow": ["did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"] }) serde_json::json!({ "type": "private", "allow": ["did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"] })
); );

View File

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::hash::Hash; use std::hash::Hash;
use std::ops::RangeBounds; use std::ops::RangeBounds;
use std::str::FromStr; use std::str::FromStr;
@ -122,7 +122,7 @@ impl Arbitrary for Visibility {
Visibility::Public Visibility::Public
} else { } else {
Visibility::Private { Visibility::Private {
allow: Vec::arbitrary(g), allow: BTreeSet::arbitrary(g),
} }
} }
} }