Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: Implement `rad block` command
Merged did:key:z6MksFqX...wzpT opened 2 years ago

Blocks repos & nodes from beeing seeded/followed.

9 files changed +201 -15 f46d396e e5fcbba4
added radicle-cli/examples/rad-block.md
@@ -0,0 +1,43 @@
+
When using an open seeding policy, it can be useful to block individual
+
repositories from being seeded.
+

+
For instance, if our default policy is to seed, any unknown repository will
+
have its policy set to allow seeding:
+
```
+
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --policy
+
Repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji is being seeded with scope `followed`
+
```
+

+
Since there is no policy specific to this repository, there's nothing to be
+
removed.
+

+
```
+
$ rad seed
+
No seeding policies to show.
+
```
+

+
But if we wanted to prevent this repository from being seeded, while
+
allowing all other repositories, we could use `rad block`:
+

+
```
+
$ rad block rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
✓ Policy for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji set to 'block'
+
```
+

+
We can see that it is now no longer seeded:
+

+
```
+
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --policy
+
Repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji is not being seeded
+
```
+

+
And a 'block' policy was added:
+

+
```
+
$ rad seed
+
╭───────────────────────────────────────────────────────╮
+
│ RID                                 Scope      Policy │
+
├───────────────────────────────────────────────────────┤
+
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   followed   block  │
+
╰───────────────────────────────────────────────────────╯
+
```
modified radicle-cli/src/commands.rs
@@ -1,5 +1,7 @@
#[path = "commands/auth.rs"]
pub mod rad_auth;
+
#[path = "commands/block.rs"]
+
pub mod rad_block;
#[path = "commands/checkout.rs"]
pub mod rad_checkout;
#[path = "commands/clean.rs"]
added radicle-cli/src/commands/block.rs
@@ -0,0 +1,96 @@
+
use std::ffi::OsString;
+

+
use radicle::node::policy::Policy;
+
use radicle::prelude::{NodeId, RepoId};
+

+
use crate::terminal as term;
+
use crate::terminal::args;
+
use crate::terminal::args::{Args, Error, Help};
+

+
pub const HELP: Help = Help {
+
    name: "block",
+
    description: "Block repositories or nodes from being seeded or followed",
+
    version: env!("CARGO_PKG_VERSION"),
+
    usage: r#"
+
Usage
+

+
    rad block <rid> [<option>...]
+
    rad block <nid> [<option>...]
+

+
    Blocks a repository from being seeded or a node from being followed.
+

+
Options
+

+
    --help          Print help
+
"#,
+
};
+

+
enum Target {
+
    Node(NodeId),
+
    Repo(RepoId),
+
}
+

+
impl std::fmt::Display for Target {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        match self {
+
            Self::Node(nid) => nid.fmt(f),
+
            Self::Repo(rid) => rid.fmt(f),
+
        }
+
    }
+
}
+

+
pub struct Options {
+
    target: Target,
+
}
+

+
impl Args for Options {
+
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
+
        use lexopt::prelude::*;
+

+
        let mut parser = lexopt::Parser::from_args(args);
+
        let mut target = None;
+

+
        while let Some(arg) = parser.next()? {
+
            match arg {
+
                Long("help") | Short('h') => {
+
                    return Err(Error::Help.into());
+
                }
+
                Value(val) if target.is_none() => {
+
                    if let Ok(rid) = args::rid(&val) {
+
                        target = Some(Target::Repo(rid));
+
                    } else if let Ok(nid) = args::nid(&val) {
+
                        target = Some(Target::Node(nid));
+
                    } else {
+
                        return Err(anyhow::anyhow!(
+
                            "invalid repository or node specified, see `rad block --help`"
+
                        ));
+
                    }
+
                }
+
                _ => return Err(anyhow::anyhow!(arg.unexpected())),
+
            }
+
        }
+

+
        Ok((
+
            Options {
+
                target: target.ok_or(anyhow::anyhow!(
+
                    "a repository or node to block must be specified, see `rad block --help`"
+
                ))?,
+
            },
+
            vec![],
+
        ))
+
    }
+
}
+

+
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
    let profile = ctx.profile()?;
+
    let mut policies = profile.policies_mut()?;
+

+
    let updated = match options.target {
+
        Target::Node(nid) => policies.set_follow_policy(&nid, Policy::Block)?,
+
        Target::Repo(rid) => policies.set_seed_policy(&rid, Policy::Block)?,
+
    };
+
    if updated {
+
        term::success!("Policy for {} set to 'block'", options.target);
+
    }
+
    Ok(())
+
}
modified radicle-cli/src/commands/help.rs
@@ -16,6 +16,7 @@ pub const HELP: Help = Help {

const COMMANDS: &[Help] = &[
    rad_auth::HELP,
+
    rad_block::HELP,
    rad_checkout::HELP,
    rad_clone::HELP,
    rad_config::HELP,
modified radicle-cli/src/commands/inspect.rs
@@ -7,17 +7,18 @@ use std::str::FromStr;
use anyhow::{anyhow, Context as _};
use chrono::prelude::*;

-
use radicle::identity::Identity;
use radicle::identity::RepoId;
+
use radicle::identity::{DocAt, Identity};
use radicle::node::policy::Policy;
use radicle::node::AliasStore as _;
+
use radicle::storage::git::{Repository, Storage};
use radicle::storage::refs::RefsAt;
use radicle::storage::{ReadRepository, ReadStorage};
-
use radicle_term::Element;

use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::json;
+
use crate::terminal::Element;

pub const HELP: Help = Help {
    name: "inspect",
@@ -141,25 +142,24 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        term::info!("{}", term::format::highlight(rid.urn()));
        return Ok(());
    }
-

    let profile = ctx.profile()?;
    let storage = &profile.storage;
-
    let repo = storage
-
        .repository(rid)
-
        .context("No repository with the given RID exists")?;
-
    let project = repo.identity_doc()?;

    match options.target {
        Target::Refs => {
+
            let (repo, _) = repo(rid, storage)?;
            refs(&repo)?;
        }
        Target::Payload => {
-
            json::to_pretty(&project.payload, Path::new("radicle.json"))?.print();
+
            let (_, doc) = repo(rid, storage)?;
+
            json::to_pretty(&doc.payload, Path::new("radicle.json"))?.print();
        }
        Target::Identity => {
-
            json::to_pretty(&project.doc, Path::new("radicle.json"))?.print();
+
            let (_, doc) = repo(rid, storage)?;
+
            json::to_pretty(&*doc, Path::new("radicle.json"))?.print();
        }
        Target::Sigrefs => {
+
            let (repo, _) = repo(rid, storage)?;
            for remote in repo.remote_ids()? {
                let remote = remote?;
                let refs = RefsAt::new(&repo, remote)?;
@@ -193,9 +193,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            }
        }
        Target::Delegates => {
+
            let (_, doc) = repo(rid, storage)?;
            let aliases = profile.aliases();
-
            for did in project.doc.delegates {
-
                if let Some(alias) = aliases.alias(&did) {
+
            for did in &doc.delegates {
+
                if let Some(alias) = aliases.alias(did) {
                    println!(
                        "{} {}",
                        term::format::tertiary(&did),
@@ -207,9 +208,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            }
        }
        Target::Visibility => {
-
            println!("{}", term::format::visibility(&project.doc.visibility));
+
            let (_, doc) = repo(rid, storage)?;
+
            println!("{}", term::format::visibility(&doc.visibility));
        }
        Target::History => {
+
            let (repo, _) = repo(rid, storage)?;
            let identity = Identity::load(&repo)?;
            let head = repo.identity_head()?;
            let history = repo.revwalk(head)?;
@@ -276,6 +279,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    Ok(())
}

+
fn repo(rid: RepoId, storage: &Storage) -> anyhow::Result<(Repository, DocAt)> {
+
    let repo = storage
+
        .repository(rid)
+
        .context("No repository with the given RID exists")?;
+
    let doc = repo.identity_doc()?;
+

+
    Ok((repo, doc))
+
}
+

fn refs(repo: &radicle::storage::git::Repository) -> anyhow::Result<()> {
    let mut refs = Vec::new();
    for r in repo.references()? {
modified radicle-cli/src/commands/seed.rs
@@ -176,7 +176,7 @@ pub fn delete(rid: RepoId, node: &mut Node, profile: &Profile) -> anyhow::Result
pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
    let store = profile.policies()?;
    let mut t = term::Table::new(term::table::TableOptions::bordered());
-
    t.push([
+
    t.header([
        term::format::default(String::from("RID")),
        term::format::default(String::from("Scope")),
        term::format::default(String::from("Policy")),
@@ -199,7 +199,12 @@ pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
            term::format::secondary(policy),
        ])
    }
-
    t.print();
+

+
    if t.is_empty() {
+
        term::print(term::format::dim("No seeding policies to show."));
+
    } else {
+
        t.print();
+
    }

    Ok(())
}
modified radicle-cli/src/main.rs
@@ -110,6 +110,13 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
                args.to_vec(),
            );
        }
+
        "block" => {
+
            term::run_command_args::<rad_block::Options, _>(
+
                rad_block::HELP,
+
                rad_block::run,
+
                args.to_vec(),
+
            );
+
        }
        "checkout" => {
            term::run_command_args::<rad_checkout::Options, _>(
                rad_checkout::HELP,
modified radicle-cli/tests/commands.rs
@@ -974,6 +974,18 @@ fn rad_unseed() {
}

#[test]
+
fn rad_block() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node(Config {
+
        policy: Policy::Allow,
+
        ..Config::test(Alias::new("alice"))
+
    });
+
    let working = tempfile::tempdir().unwrap();
+

+
    test("examples/rad-block.md", working, Some(&alice.home), []).unwrap();
+
}
+

+
#[test]
fn rad_clone() {
    let mut environment = Environment::new();
    let mut alice = environment.node(Config::test(Alias::new("alice")));
modified radicle-term/src/table.rs
@@ -56,6 +56,7 @@ impl TableOptions {

#[derive(Debug)]
enum Row<const W: usize, T> {
+
    Header([T; W]),
    Data([T; W]),
    Divider,
}
@@ -104,7 +105,7 @@ where
            let mut line = Line::default();

            match row {
-
                Row::Data(cells) => {
+
                Row::Header(cells) | Row::Data(cells) => {
                    if let Some(color) = border {
                        line.push(Paint::new("│ ").fg(color));
                    }
@@ -178,6 +179,13 @@ impl<const W: usize, T: Cell> Table<W, T> {
        self.rows.push(Row::Data(row));
    }

+
    pub fn header(&mut self, row: [T; W]) {
+
        for (i, cell) in row.iter().enumerate() {
+
            self.widths[i] = self.widths[i].max(cell.width());
+
        }
+
        self.rows.push(Row::Header(row));
+
    }
+

    pub fn extend(&mut self, rows: impl IntoIterator<Item = [T; W]>) {
        for row in rows.into_iter() {
            self.push(row);