cli: rework `rad rm` as `rad clean`

Removing a repository completely can result in data corruption for
users, since a user's `rad/sigrefs` are the ground truth of their
state, and any fork of them results in errors in the protocol.

Instead of `rad rm`, we replace it with `rad clean` that removes all
remotes other than the local peer's namespace and the delegates'. The
delegates are also necessary to keep, since the repository's identity
is dependent on them.

Note that the remotes that were cleaned will be populated again upon
fetching, as long as they are tracked.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2023-11-22 14:14:11 +00:00
parent d23120ca8b
commit 0fb1128bd3
No known key found for this signature in database
GPG Key ID: 582959CEFC92CB4E
7 changed files with 120 additions and 97 deletions

View File

@ -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
```

View File

@ -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
```

View File

@ -4,6 +4,8 @@ pub mod rad_assign;
pub mod rad_auth; pub mod rad_auth;
#[path = "commands/checkout.rs"] #[path = "commands/checkout.rs"]
pub mod rad_checkout; pub mod rad_checkout;
#[path = "commands/clean.rs"]
pub mod rad_clean;
#[path = "commands/clone.rs"] #[path = "commands/clone.rs"]
pub mod rad_clone; pub mod rad_clone;
#[path = "commands/cob.rs"] #[path = "commands/cob.rs"]
@ -38,8 +40,6 @@ pub mod rad_publish;
pub mod rad_remote; pub mod rad_remote;
#[path = "commands/review.rs"] #[path = "commands/review.rs"]
pub mod rad_review; pub mod rad_review;
#[path = "commands/rm.rs"]
pub mod rad_rm;
#[path = "commands/self.rs"] #[path = "commands/self.rs"]
pub mod rad_self; pub mod rad_self;
#[path = "commands/sync.rs"] #[path = "commands/sync.rs"]

View File

@ -3,26 +3,26 @@ use std::ffi::OsString;
use anyhow::anyhow; use anyhow::anyhow;
use radicle::identity::Id; use radicle::identity::Id;
use radicle::node;
use radicle::node::Handle as _;
use radicle::storage; use radicle::storage;
use radicle::storage::WriteStorage; use radicle::storage::WriteStorage;
use radicle::Profile;
use crate::git;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help { pub const HELP: Help = Help {
name: "rm", name: "clean",
description: "Remove a project", description: "Clean a project",
version: env!("CARGO_PKG_VERSION"), version: env!("CARGO_PKG_VERSION"),
usage: r#" usage: r#"
Usage Usage
rad rm <rid> [<option>...] rad clean <rid> [<option>...]
Removes a repository from storage. The repository is also untracked, if possible. 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 Options
@ -61,7 +61,8 @@ impl Args for Options {
Ok(( Ok((
Options { Options {
rid: id.ok_or_else(|| anyhow!("an RID must be provided; see `rad rm --help`"))?, rid: id
.ok_or_else(|| anyhow!("an RID must be provided; see `rad clean --help`"))?,
confirm, confirm,
}, },
vec![], vec![],
@ -80,48 +81,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
} }
if !options.confirm || term::confirm(format!("Remove {rid}?")) { if !options.confirm || term::confirm(format!("Remove {rid}?")) {
untrack(&rid, &profile)?; let cleaned = storage.clean(rid)?;
remove_remote(&rid)?; for remote in cleaned {
storage.remove(rid)?; term::info!("Removed {remote}");
term::success!("Successfully removed {rid} from storage"); }
} term::success!("Successfully cleaned {rid}");
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(()) Ok(())

View File

@ -28,7 +28,7 @@ const COMMANDS: &[Help] = &[
rad_patch::HELP, rad_patch::HELP,
rad_path::HELP, rad_path::HELP,
rad_review::HELP, rad_review::HELP,
rad_rm::HELP, rad_clean::HELP,
rad_self::HELP, rad_self::HELP,
rad_label::HELP, rad_label::HELP,
rad_track::HELP, rad_track::HELP,

View File

@ -215,8 +215,12 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(), args.to_vec(),
); );
} }
"rm" => { "clean" => {
term::run_command_args::<rad_rm::Options, _>(rad_rm::HELP, rad_rm::run, args.to_vec()); term::run_command_args::<rad_clean::Options, _>(
rad_clean::HELP,
rad_clean::run,
args.to_vec(),
);
} }
"self" => { "self" => {
term::run_command_args::<rad_self::Options, _>( term::run_command_args::<rad_self::Options, _>(

View File

@ -623,17 +623,47 @@ fn rad_review_by_hunk() {
} }
#[test] #[test]
fn rad_rm() { fn rad_clean() {
let mut environment = Environment::new(); let mut environment = Environment::new();
let profile = environment.profile("alice"); let alice = environment.node(Config::test(Alias::new("alice")));
let working = tempfile::tempdir().unwrap(); let bob = environment.node(Config::test(Alias::new("bob")));
let home = &profile.home; let working = environment.tmp().join("working");
// Setup a test repository. // Setup a test project.
fixtures::repository(working.path()); 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(); let mut alice = alice.spawn();
test("examples/rad-rm.md", working.path(), Some(home), []).unwrap(); 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] #[test]