Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: rework `rad rm` as `rad clean`
Fintan Halpenny committed 2 years ago
commit 0fb1128bd319155d400f463eb45aaffd7fa07d1a
parent d23120ca8bf082e7dc7171a3b53829593cbf7701
8 files changed +197 -174
added radicle-cli/examples/rad-clean.md
@@ -0,0 +1,58 @@
+
We cannot delete a repository, since that can cause data integrity
+
issues. However, we can clean the storage of remotes that are not the
+
local peer or the repository delegates. To do this we can use the `rad
+
clean` command.
+

+
First let's look at what we have locally:
+

+
```
+
$ rad ls
+
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ Name        RID                                 Visibility   Head      Description                        │
+
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
```
+

+
Let's also inspect what remotes are in the repository:
+

+
```
+
$ rad inspect --sigrefs
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f209c9f68aa689af24220a20462e13ee9dfb2a95
+
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk 161b775a3509c8098de67f57f750972bba015b31
+
```
+

+
Now let's clean the `heartwood` project:
+

+
```
+
$ rad clean rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
+
Removed z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
✓ Successfully cleaned rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
```
+

+
Inspecting the remotes again, we see that Bob is now gone:
+

+
```
+
$ rad inspect --sigrefs
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f209c9f68aa689af24220a20462e13ee9dfb2a95
+
```
+

+
Note that Bob will be fetched again if we do not untrack his
+
node. Currently, there is no per repository tracking so it's not
+
possible to stop fetching Bob for this particular repository.
+

+
Cleaning a repository again will remove no remotes, since we're
+
already at the minimal set of remotes:
+

+
```
+
$ rad clean rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
+
✓ Successfully cleaned rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
```
+

+
And attempting to clean a non-existent repository has no effect on the
+
storage at all:
+

+
``` (fail)
+
$ rad clean rad:z42hL2jL4XNk6K8oHQaSWdeadbeef --no-confirm
+
✗ Error: repository rad:z42hL2jL4XNk6K8oHQaSWdeadbeef was not found
+
```
deleted radicle-cli/examples/rad-rm.md
@@ -1,33 +0,0 @@
-
To delete a repository from local storage, we use the `rad rm` command.
-
First let's look at what we have locally:
-

-
```
-
$ rad ls
-
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ Name        RID                                 Visibility   Head      Description                        │
-
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
-
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
-
```
-

-
Now let's delete the `heartwood` project:
-

-
```
-
$ rad rm rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
-
✓ Untracked rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Successfully removed 'rad' remote
-
✓ Successfully removed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from storage
-
```
-

-
We can check our repositories again to see if it was deleted:
-

-
```
-
$ rad ls
-
```
-

-
Attempting to remove a repository that doesn't exist gives us an error message:
-

-
``` (fail)
-
$ rad rm rad:z2Jk1mNqyX7AjT4K83jJW9vQoHn4f
-
✗ Error: repository rad:z2Jk1mNqyX7AjT4K83jJW9vQoHn4f was not found
-
```
modified radicle-cli/src/commands.rs
@@ -4,6 +4,8 @@ pub mod rad_assign;
pub mod rad_auth;
#[path = "commands/checkout.rs"]
pub mod rad_checkout;
+
#[path = "commands/clean.rs"]
+
pub mod rad_clean;
#[path = "commands/clone.rs"]
pub mod rad_clone;
#[path = "commands/cob.rs"]
@@ -38,8 +40,6 @@ pub mod rad_publish;
pub mod rad_remote;
#[path = "commands/review.rs"]
pub mod rad_review;
-
#[path = "commands/rm.rs"]
-
pub mod rad_rm;
#[path = "commands/self.rs"]
pub mod rad_self;
#[path = "commands/sync.rs"]
added radicle-cli/src/commands/clean.rs
@@ -0,0 +1,92 @@
+
use std::ffi::OsString;
+

+
use anyhow::anyhow;
+

+
use radicle::identity::Id;
+
use radicle::storage;
+
use radicle::storage::WriteStorage;
+

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

+
pub const HELP: Help = Help {
+
    name: "clean",
+
    description: "Clean a project",
+
    version: env!("CARGO_PKG_VERSION"),
+
    usage: r#"
+
Usage
+

+
    rad clean <rid> [<option>...]
+

+
    Removes all remotes from a repository, as long as they are not the
+
    local operator or a delegate of the repository.
+

+
    Note that remotes will still be fetched as long as they are
+
    tracked and/or the tracking scope is "all".
+

+
Options
+

+
    --no-confirm        Do not ask for confirmation before removal (default: false)
+
    --help              Print help
+
"#,
+
};
+

+
pub struct Options {
+
    rid: Id,
+
    confirm: bool,
+
}
+

+
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 id: Option<Id> = None;
+
        let mut confirm = true;
+

+
        while let Some(arg) = parser.next()? {
+
            match arg {
+
                Long("no-confirm") => {
+
                    confirm = false;
+
                }
+
                Long("help") | Short('h') => {
+
                    return Err(Error::Help.into());
+
                }
+
                Value(val) if id.is_none() => {
+
                    id = Some(term::args::rid(&val)?);
+
                }
+
                _ => return Err(anyhow::anyhow!(arg.unexpected())),
+
            }
+
        }
+

+
        Ok((
+
            Options {
+
                rid: id
+
                    .ok_or_else(|| anyhow!("an RID must be provided; see `rad clean --help`"))?,
+
                confirm,
+
            },
+
            vec![],
+
        ))
+
    }
+
}
+

+
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
    let profile = ctx.profile()?;
+
    let storage = &profile.storage;
+
    let rid = options.rid;
+
    let path = storage::git::paths::repository(storage, &rid);
+

+
    if !path.exists() {
+
        anyhow::bail!("repository {rid} was not found");
+
    }
+

+
    if !options.confirm || term::confirm(format!("Remove {rid}?")) {
+
        let cleaned = storage.clean(rid)?;
+
        for remote in cleaned {
+
            term::info!("Removed {remote}");
+
        }
+
        term::success!("Successfully cleaned {rid}");
+
    }
+

+
    Ok(())
+
}
modified radicle-cli/src/commands/help.rs
@@ -28,7 +28,7 @@ const COMMANDS: &[Help] = &[
    rad_patch::HELP,
    rad_path::HELP,
    rad_review::HELP,
-
    rad_rm::HELP,
+
    rad_clean::HELP,
    rad_self::HELP,
    rad_label::HELP,
    rad_track::HELP,
deleted radicle-cli/src/commands/rm.rs
@@ -1,128 +0,0 @@
-
use std::ffi::OsString;
-

-
use anyhow::anyhow;
-

-
use radicle::identity::Id;
-
use radicle::node;
-
use radicle::node::Handle as _;
-
use radicle::storage;
-
use radicle::storage::WriteStorage;
-
use radicle::Profile;
-

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

-
pub const HELP: Help = Help {
-
    name: "rm",
-
    description: "Remove a project",
-
    version: env!("CARGO_PKG_VERSION"),
-
    usage: r#"
-
Usage
-

-
    rad rm <rid> [<option>...]
-

-
    Removes a repository from storage. The repository is also untracked, if possible.
-

-
Options
-

-
    --no-confirm        Do not ask for confirmation before removal (default: false)
-
    --help              Print help
-
"#,
-
};
-

-
pub struct Options {
-
    rid: Id,
-
    confirm: bool,
-
}
-

-
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 id: Option<Id> = None;
-
        let mut confirm = true;
-

-
        while let Some(arg) = parser.next()? {
-
            match arg {
-
                Long("no-confirm") => {
-
                    confirm = false;
-
                }
-
                Long("help") | Short('h') => {
-
                    return Err(Error::Help.into());
-
                }
-
                Value(val) if id.is_none() => {
-
                    id = Some(term::args::rid(&val)?);
-
                }
-
                _ => return Err(anyhow::anyhow!(arg.unexpected())),
-
            }
-
        }
-

-
        Ok((
-
            Options {
-
                rid: id.ok_or_else(|| anyhow!("an RID must be provided; see `rad rm --help`"))?,
-
                confirm,
-
            },
-
            vec![],
-
        ))
-
    }
-
}
-

-
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
-
    let profile = ctx.profile()?;
-
    let storage = &profile.storage;
-
    let rid = options.rid;
-
    let path = storage::git::paths::repository(storage, &rid);
-

-
    if !path.exists() {
-
        anyhow::bail!("repository {rid} was not found");
-
    }
-

-
    if !options.confirm || term::confirm(format!("Remove {rid}?")) {
-
        untrack(&rid, &profile)?;
-
        remove_remote(&rid)?;
-
        storage.remove(rid)?;
-
        term::success!("Successfully removed {rid} from storage");
-
    }
-

-
    Ok(())
-
}
-

-
fn untrack(rid: &Id, profile: &Profile) -> anyhow::Result<()> {
-
    let mut node = radicle::Node::new(profile.socket());
-

-
    let result = if node.is_running() {
-
        node.untrack_repo(*rid).map_err(anyhow::Error::from)
-
    } else {
-
        let mut store =
-
            node::tracking::store::Config::open(profile.home.node().join(node::TRACKING_DB_FILE))?;
-
        store.untrack_repo(rid).map_err(anyhow::Error::from)
-
    };
-

-
    if let Err(e) = result {
-
        term::warning(format!("Failed to untrack repository: {e}"));
-
        term::warning("Make sure to untrack this repository when your node is running");
-
    } else {
-
        term::success!("Untracked {rid}")
-
    }
-

-
    Ok(())
-
}
-

-
fn remove_remote(rid: &Id) -> anyhow::Result<()> {
-
    let cwd = std::env::current_dir()?;
-
    if let Err(e) = git::Repository::open(cwd)
-
        .map_err(|err| err.into())
-
        .and_then(|repo| git::remove_remote(&repo, rid))
-
    {
-
        term::warning(format!(
-
            "Attempted to remove 'rad' remote from working copy: {e}"
-
        ));
-
        term::warning("In case a working copy exists, make sure to `git remote remove rad`");
-
    } else {
-
        term::success!("Successfully removed 'rad' remote");
-
    }
-

-
    Ok(())
-
}
modified radicle-cli/src/main.rs
@@ -215,8 +215,12 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
                args.to_vec(),
            );
        }
-
        "rm" => {
-
            term::run_command_args::<rad_rm::Options, _>(rad_rm::HELP, rad_rm::run, args.to_vec());
+
        "clean" => {
+
            term::run_command_args::<rad_clean::Options, _>(
+
                rad_clean::HELP,
+
                rad_clean::run,
+
                args.to_vec(),
+
            );
        }
        "self" => {
            term::run_command_args::<rad_self::Options, _>(
modified radicle-cli/tests/commands.rs
@@ -623,17 +623,47 @@ fn rad_review_by_hunk() {
}

#[test]
-
fn rad_rm() {
+
fn rad_clean() {
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-
    let working = tempfile::tempdir().unwrap();
-
    let home = &profile.home;
+
    let alice = environment.node(Config::test(Alias::new("alice")));
+
    let bob = environment.node(Config::test(Alias::new("bob")));
+
    let working = environment.tmp().join("working");

-
    // Setup a test repository.
-
    fixtures::repository(working.path());
+
    // Setup a test project.
+
    let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    fixtures::repository(working.join("acme"));
+
    test(
+
        "examples/rad-init.md",
+
        working.join("acme"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();

-
    test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
-
    test("examples/rad-rm.md", working.path(), Some(home), []).unwrap();
+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    alice.handle.track_repo(acme, Scope::All).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test(
+
        "examples/rad-clone.md",
+
        working.join("bob"),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    bob.has_inventory_of(&acme, &alice.id);
+
    alice.has_inventory_of(&acme, &bob.id);
+

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

#[test]