cli: list tracked and untracked remotes

Add functionality for lising the remotes that are in the storage
repository, but a working copy remote has not yet been created for
them.

This functionality is included in the `rad remote` command since it
can be directly used for creating Git remotes using the same command,
i.e. `rad remote add`.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2023-12-14 09:37:05 +00:00 committed by cloudhead
parent e3b3f3c4f4
commit 3cf51043b3
No known key found for this signature in database
4 changed files with 225 additions and 30 deletions

View File

@ -51,3 +51,44 @@ $ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
```
We can also use `rad remote` to list all the remotes that are
available in the repository by using the `--untracked` flag:
```
$ rad remote --untracked
eve did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
```
If we use `--all`, then we can see all the remotes that we have
created in the working copy, followed by all the available remotes:
```
$ rad remote --all
bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (fetch)
rad (canonical upstream) (fetch)
rad z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
eve did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
```
As we can see, we have also have another remote namespace `eve`, so
let's add them to our set of working copy remotes:
```
$ rad remote add did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --name eve
✓ Remote eve added
✓ Remote-tracking branch eve/master created for z6Mkux1…nVhib7Z
```
After adding `eve`'s remote, we no longer see any entries that are
untracked:
```
$ rad remote --all
bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (fetch)
eve z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z (fetch)
rad (canonical upstream) (fetch)
rad z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
```

View File

@ -14,6 +14,7 @@ use radicle::git::RefString;
use radicle::prelude::NodeId;
use radicle::storage::ReadStorage;
use crate::terminal as term;
use crate::terminal::args;
use crate::terminal::{Args, Context, Help};
@ -24,19 +25,28 @@ pub const HELP: Help = Help {
usage: r#"
Usage
rad remote
rad remote list
rad remote add (<did> | <nid>) [--name <string>]
rad remote rm <name>
rad remote [<option>...]
rad remote list [--tracked | --untracked | --all] [<option>...]
rad remote add (<did> | <nid>) [--name <string>] [<option>...]
rad remote rm <name> [<option>...]
List options
--tracked Show all remotes that are listed in the working copy
--untracked Show all remotes that are listed in the Radicle storage
--all Show all remotes in both the Radicle storage and the working copy
Add options
--name Override the name of the remote that by default is set to the node alias
Options
--name Override the name of the remote that by default is set to the node alias
--help Print help
--help Print help
"#,
};
#[derive(Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OperationName {
Add,
Rm,
@ -48,7 +58,15 @@ pub enum OperationName {
pub enum Operation {
Add { id: NodeId, name: Option<RefString> },
Rm { name: RefString },
List,
List { option: ListOption },
}
#[derive(Debug, Default)]
pub enum ListOption {
All,
#[default]
Tracked,
Untracked,
}
#[derive(Debug)]
@ -64,6 +82,7 @@ impl Args for Options {
let mut op: Option<OperationName> = None;
let mut id: Option<NodeId> = None;
let mut name: Option<RefString> = None;
let mut list_op: ListOption = ListOption::default();
while let Some(arg) = parser.next()? {
match arg {
@ -82,10 +101,25 @@ impl Args for Options {
"r" | "rm" => op = Some(OperationName::Rm),
unknown => anyhow::bail!("unknown operation '{}'", unknown),
},
// List options
Long("all") if op.unwrap_or_default() == OperationName::List => {
list_op = ListOption::All;
}
Long("tracked") if op.unwrap_or_default() == OperationName::List => {
list_op = ListOption::Tracked;
}
Long("untracked") if op.unwrap_or_default() == OperationName::List => {
list_op = ListOption::Untracked;
}
// Add options
Value(val) if op == Some(OperationName::Add) && id.is_none() => {
let nid = args::pubkey(&val)?;
id = Some(nid);
}
// Remove options
Value(val) if op == Some(OperationName::Rm) && name.is_none() => {
let val = args::string(&val);
let val = RefString::try_from(val)
@ -104,7 +138,7 @@ impl Args for Options {
))?,
name,
},
OperationName::List => Operation::List,
OperationName::List => Operation::List { option: list_op },
OperationName::Rm => Operation::Rm {
name: name.ok_or(anyhow!("name required, see `rad remote`"))?,
},
@ -127,7 +161,29 @@ pub fn run(options: Options, ctx: impl Context) -> anyhow::Result<()> {
self::add::run(rid, id, name, Some(branch.clone()), &profile, &working)?
}
Operation::Rm { ref name } => self::rm::run(name, &working)?,
Operation::List => self::list::run(&working)?,
Operation::List { option } => match option {
ListOption::All => {
let tracked = list::tracked(&working)?;
let untracked = list::untracked(rid, &profile, tracked.iter())?;
// Only include a blank line if we're printing both tracked and untracked
let include_blank_line = !tracked.is_empty() && !untracked.is_empty();
list::print_tracked(tracked.iter());
if include_blank_line {
term::blank();
}
list::print_untracked(untracked.iter());
}
ListOption::Tracked => {
let tracked = list::tracked(&working)?;
list::print_tracked(tracked.iter());
}
ListOption::Untracked => {
let tracked = list::tracked(&working)?;
let untracked = list::untracked(rid, &profile, tracked.iter())?;
list::print_untracked(untracked.iter());
}
},
};
Ok(())
}

View File

@ -1,29 +1,119 @@
use std::collections::HashSet;
use radicle::git::Url;
use radicle::identity::{Did, Id};
use radicle::node::{Alias, AliasStore as _, NodeId};
use radicle::storage::ReadStorage as _;
use radicle::Profile;
use radicle_term::{Element, Table};
use crate::git;
use crate::terminal as term;
pub fn run(repo: &git::Repository) -> anyhow::Result<()> {
let mut table = Table::default();
let remotes = git::rad_remotes(repo)?;
for r in remotes {
for (dir, url) in [("fetch", Some(r.url)), ("push", r.pushurl)] {
let Some(url) = url else {
continue;
};
#[derive(Debug)]
pub enum Direction {
Push(Url),
Fetch(Url),
}
let description = url.namespace.map_or(
term::format::dim("(canonical upstream)".to_string()).italic(),
|namespace| term::format::tertiary(namespace.to_string()),
);
table.push([
term::format::bold(r.name.clone()),
description,
term::format::parens(term::format::secondary(dir.to_owned())),
]);
#[derive(Debug)]
pub struct Tracked {
name: String,
direction: Option<Direction>,
}
impl Tracked {
fn namespace(&self) -> Option<NodeId> {
match self.direction.as_ref()? {
Direction::Push(url) => url.namespace,
Direction::Fetch(url) => url.namespace,
}
}
table.print();
Ok(())
}
#[derive(Debug)]
pub struct Untracked {
remote: NodeId,
alias: Option<Alias>,
}
pub fn tracked(working: &git::Repository) -> anyhow::Result<Vec<Tracked>> {
Ok(git::rad_remotes(working)?
.into_iter()
.flat_map(|remote| {
[
Tracked {
name: remote.name.clone(),
direction: Some(Direction::Fetch(remote.url)),
},
Tracked {
name: remote.name,
direction: remote.pushurl.map(Direction::Push),
},
]
})
.collect())
}
pub fn untracked<'a>(
rid: Id,
profile: &Profile,
tracked: impl Iterator<Item = &'a Tracked>,
) -> anyhow::Result<Vec<Untracked>> {
let repo = profile.storage.repository(rid)?;
let aliases = profile.aliases();
let remotes = repo.remote_ids()?;
let git_remotes = tracked
.filter_map(|tracked| tracked.namespace())
.collect::<HashSet<_>>();
Ok(remotes
.filter_map(|remote| {
remote
.map(|remote| {
(!git_remotes.contains(&remote)).then_some(Untracked {
remote,
alias: aliases.alias(&remote),
})
})
.transpose()
})
.collect::<Result<Vec<_>, _>>()?)
}
pub fn print_tracked<'a>(tracked: impl Iterator<Item = &'a Tracked>) {
let mut table = Table::default();
for Tracked { direction, name } in tracked {
let Some(direction) = direction else {
continue;
};
let (dir, url) = match direction {
Direction::Push(url) => ("push", url),
Direction::Fetch(url) => ("fetch", url),
};
let description = url.namespace.map_or(
term::format::dim("(canonical upstream)".to_string()).italic(),
|namespace| term::format::tertiary(namespace.to_string()),
);
table.push([
term::format::bold(name.clone()),
description,
term::format::parens(term::format::secondary(dir.to_owned())),
]);
}
table.print();
}
pub fn print_untracked<'a>(untracked: impl Iterator<Item = &'a Untracked>) {
let mut t = Table::default();
for Untracked { remote, alias } in untracked {
t.push([
match alias {
None => term::format::secondary("n/a".to_string()),
Some(alias) => term::format::secondary(alias.to_string()),
},
term::format::highlight(Did::from(remote).to_string()),
])
}
t.print();
}

View File

@ -1475,6 +1475,7 @@ fn rad_remote() {
let mut environment = Environment::new();
let alice = environment.node(Config::test(Alias::new("alice")));
let bob = environment.node(Config::test(Alias::new("bob")));
let eve = environment.node(Config::test(Alias::new("eve")));
let working = environment.tmp().join("working");
let home = alice.home.clone();
let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
@ -1491,6 +1492,7 @@ fn rad_remote() {
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let mut eve = eve.spawn();
alice
.handle
.follow(bob.id, Some(Alias::new("bob")))
@ -1502,6 +1504,12 @@ fn rad_remote() {
bob.announce(rid, bob.home.path()).unwrap();
alice.has_inventory_of(&rid, &bob.id);
eve.connect(&bob);
eve.routes_to(&[(rid, alice.id)]);
eve.fork(rid, eve.home.path()).unwrap();
eve.announce(rid, eve.home.path()).unwrap();
alice.has_inventory_of(&rid, &eve.id);
test(
"examples/rad-remote.md",
working.join("alice"),