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:
parent
e3b3f3c4f4
commit
3cf51043b3
|
|
@ -51,3 +51,44 @@ $ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
|
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
|
||||||
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
|
✓ 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use radicle::git::RefString;
|
||||||
use radicle::prelude::NodeId;
|
use radicle::prelude::NodeId;
|
||||||
use radicle::storage::ReadStorage;
|
use radicle::storage::ReadStorage;
|
||||||
|
|
||||||
|
use crate::terminal as term;
|
||||||
use crate::terminal::args;
|
use crate::terminal::args;
|
||||||
use crate::terminal::{Args, Context, Help};
|
use crate::terminal::{Args, Context, Help};
|
||||||
|
|
||||||
|
|
@ -24,19 +25,28 @@ pub const HELP: Help = Help {
|
||||||
usage: r#"
|
usage: r#"
|
||||||
Usage
|
Usage
|
||||||
|
|
||||||
rad remote
|
rad remote [<option>...]
|
||||||
rad remote list
|
rad remote list [--tracked | --untracked | --all] [<option>...]
|
||||||
rad remote add (<did> | <nid>) [--name <string>]
|
rad remote add (<did> | <nid>) [--name <string>] [<option>...]
|
||||||
rad remote rm <name>
|
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
|
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 {
|
pub enum OperationName {
|
||||||
Add,
|
Add,
|
||||||
Rm,
|
Rm,
|
||||||
|
|
@ -48,7 +58,15 @@ pub enum OperationName {
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
Add { id: NodeId, name: Option<RefString> },
|
Add { id: NodeId, name: Option<RefString> },
|
||||||
Rm { name: RefString },
|
Rm { name: RefString },
|
||||||
List,
|
List { option: ListOption },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum ListOption {
|
||||||
|
All,
|
||||||
|
#[default]
|
||||||
|
Tracked,
|
||||||
|
Untracked,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -64,6 +82,7 @@ impl Args for Options {
|
||||||
let mut op: Option<OperationName> = None;
|
let mut op: Option<OperationName> = None;
|
||||||
let mut id: Option<NodeId> = None;
|
let mut id: Option<NodeId> = None;
|
||||||
let mut name: Option<RefString> = None;
|
let mut name: Option<RefString> = None;
|
||||||
|
let mut list_op: ListOption = ListOption::default();
|
||||||
|
|
||||||
while let Some(arg) = parser.next()? {
|
while let Some(arg) = parser.next()? {
|
||||||
match arg {
|
match arg {
|
||||||
|
|
@ -82,10 +101,25 @@ impl Args for Options {
|
||||||
"r" | "rm" => op = Some(OperationName::Rm),
|
"r" | "rm" => op = Some(OperationName::Rm),
|
||||||
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
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() => {
|
Value(val) if op == Some(OperationName::Add) && id.is_none() => {
|
||||||
let nid = args::pubkey(&val)?;
|
let nid = args::pubkey(&val)?;
|
||||||
id = Some(nid);
|
id = Some(nid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove options
|
||||||
Value(val) if op == Some(OperationName::Rm) && name.is_none() => {
|
Value(val) if op == Some(OperationName::Rm) && name.is_none() => {
|
||||||
let val = args::string(&val);
|
let val = args::string(&val);
|
||||||
let val = RefString::try_from(val)
|
let val = RefString::try_from(val)
|
||||||
|
|
@ -104,7 +138,7 @@ impl Args for Options {
|
||||||
))?,
|
))?,
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
OperationName::List => Operation::List,
|
OperationName::List => Operation::List { option: list_op },
|
||||||
OperationName::Rm => Operation::Rm {
|
OperationName::Rm => Operation::Rm {
|
||||||
name: name.ok_or(anyhow!("name required, see `rad remote`"))?,
|
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)?
|
self::add::run(rid, id, name, Some(branch.clone()), &profile, &working)?
|
||||||
}
|
}
|
||||||
Operation::Rm { ref name } => self::rm::run(name, &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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 radicle_term::{Element, Table};
|
||||||
|
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
|
||||||
pub fn run(repo: &git::Repository) -> anyhow::Result<()> {
|
#[derive(Debug)]
|
||||||
let mut table = Table::default();
|
pub enum Direction {
|
||||||
let remotes = git::rad_remotes(repo)?;
|
Push(Url),
|
||||||
for r in remotes {
|
Fetch(Url),
|
||||||
for (dir, url) in [("fetch", Some(r.url)), ("push", r.pushurl)] {
|
}
|
||||||
let Some(url) = url else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let description = url.namespace.map_or(
|
#[derive(Debug)]
|
||||||
term::format::dim("(canonical upstream)".to_string()).italic(),
|
pub struct Tracked {
|
||||||
|namespace| term::format::tertiary(namespace.to_string()),
|
name: String,
|
||||||
);
|
direction: Option<Direction>,
|
||||||
table.push([
|
}
|
||||||
term::format::bold(r.name.clone()),
|
|
||||||
description,
|
impl Tracked {
|
||||||
term::format::parens(term::format::secondary(dir.to_owned())),
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1475,6 +1475,7 @@ fn rad_remote() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
let alice = environment.node(Config::test(Alias::new("alice")));
|
||||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
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 working = environment.tmp().join("working");
|
||||||
let home = alice.home.clone();
|
let home = alice.home.clone();
|
||||||
let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||||
|
|
@ -1491,6 +1492,7 @@ fn rad_remote() {
|
||||||
|
|
||||||
let mut alice = alice.spawn();
|
let mut alice = alice.spawn();
|
||||||
let mut bob = bob.spawn();
|
let mut bob = bob.spawn();
|
||||||
|
let mut eve = eve.spawn();
|
||||||
alice
|
alice
|
||||||
.handle
|
.handle
|
||||||
.follow(bob.id, Some(Alias::new("bob")))
|
.follow(bob.id, Some(Alias::new("bob")))
|
||||||
|
|
@ -1502,6 +1504,12 @@ fn rad_remote() {
|
||||||
bob.announce(rid, bob.home.path()).unwrap();
|
bob.announce(rid, bob.home.path()).unwrap();
|
||||||
alice.has_inventory_of(&rid, &bob.id);
|
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(
|
test(
|
||||||
"examples/rad-remote.md",
|
"examples/rad-remote.md",
|
||||||
working.join("alice"),
|
working.join("alice"),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue