cli: Improve `rad remote add`

By default, this command now:
* Follows the peer if not already followed
* Syncs refs of the peer, if it wasn't followed before
* Fetches refs into working copy

This functionality can be disabled via `--no-sync` and `--no-fetch`.
This commit is contained in:
cloudhead 2024-01-04 16:39:21 +01:00
parent 3a3820b06d
commit e4f06e9c9d
No known key found for this signature in database
15 changed files with 130 additions and 85 deletions

View File

@ -7,9 +7,6 @@ First we add a second delegate, Bob, to our repo:
``` ~alice
$ rad id update --title "Add Bob" --description "" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji -q
c036c0d89ce26aef3ad7da402157dba16b5163b4
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
```
Then, as Bob, we commit some code on top of the canonical head:
@ -32,7 +29,9 @@ As Alice, we fetch that code, but commit on top of our own master, which is no
longer canonical, since Bob pushed a more recent commit, and the threshold is 1:
``` ~alice
$ git fetch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --fetch --no-sync
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
$ git branch -arv
bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master 319a7dc Third commit
rad/master f2de534 Second commit

View File

@ -48,18 +48,9 @@ z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
We can then setup a git remote for `bob`:
```
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob --no-sync --fetch
✓ Remote bob added
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
```
And fetch his refs:
```
$ git fetch --all
Fetching rad
Fetching alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
Fetching bob
$ git branch --remotes
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
bob/master

View File

@ -8,9 +8,6 @@ $ git push -o patch.message="Changes" rad HEAD:refs/patches
```
``` ~alice
$ rad sync -f
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
✓ Fetched repository from 1 seed(s)
$ git checkout -b alice/1 -q
$ git rev-parse HEAD
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
@ -19,13 +16,12 @@ $ rad patch checkout 0fd67a0
✓ Switched to branch patch/0fd67a0
✓ Branch patch/0fd67a0 setup to track rad/patches/0fd67a0364af1f79ed8770a35ed09d85571d4c21
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
$ git checkout master -q
$ git fetch --all -q
$ cat .git/FETCH_HEAD
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 branch 'master' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
8d5f1bae4b69d8e3f6cbfc6f4bd675ed19990afc not-for-merge branch 'patches/0fd67a0364af1f79ed8770a35ed09d85571d4c21' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 not-for-merge branch 'master' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
8d5f1bae4b69d8e3f6cbfc6f4bd675ed19990afc not-for-merge branch 'patches/0fd67a0364af1f79ed8770a35ed09d85571d4c21' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
$ git rev-parse master

View File

@ -1,7 +1,7 @@
Now, let's add a bob as a new remote:
```
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob --no-sync
✓ Remote bob added
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
```
@ -18,14 +18,6 @@ rad z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
You can see both `bob` and `rad` as remotes. The `rad` remote is our personal
remote of the project.
For the remote-tracking branch to work, we fetch bob:
``` RAD_SOCKET=/dev/null (stderr)
$ git fetch bob
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new branch] master -> bob/master
```
We can now see the remote-tracking branch that was setup:
```
@ -47,7 +39,7 @@ Now, add another time `bob` but without specify the `name`, so we should be
able to fetch the node alias from our db!
```
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-sync
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
```
@ -76,7 +68,7 @@ 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
$ rad remote add did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --name eve --no-sync
✓ Remote eve added
✓ Remote-tracking branch eve/master created for z6Mkux1…nVhib7Z
```

View File

@ -1,29 +1,16 @@
Back to being the project maintainer.
Changes have been proposed by another person (or peer) via a radicle patch. To
follow changes by another, we must 'follow' them.
Changes have been proposed by another peer via a radicle patch. To track
changes from another peer, we must first follow them, and then create
a tracking branch in our working copy. The `rad remote add` command does all
of this.
```
$ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias bob
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob --sync --fetch
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
```
Additionally, we need to add a new 'git remote' to our working copy for the
peer. Upcoming versions of radicle will not require this step.
```
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
✓ Remote bob added
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
$ rad sync -f
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
✓ Fetched repository from 1 seed(s)
```
``` (stderr)
$ git fetch bob
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
* [new branch] patches/3581e83ad18f5cdd806ab50fa11cfd5dd4e8ae1c -> bob/patches/3581e83ad18f5cdd806ab50fa11cfd5dd4e8ae1c
```
The contributor's changes are now visible to us.

View File

@ -156,7 +156,7 @@ pub fn setup_remote(
remote_id: &NodeId,
remote_name: Option<git::RefString>,
aliases: &impl AliasStore,
) -> anyhow::Result<()> {
) -> anyhow::Result<git::RefString> {
let remote_name = if let Some(name) = remote_name {
name
} else {
@ -168,7 +168,7 @@ pub fn setup_remote(
git::RefString::try_from(name.as_str())
.map_err(|_| anyhow!("invalid remote name: '{name}'"))?
};
let (remote, branch) = setup.run(remote_name, *remote_id)?;
let (remote, branch) = setup.run(&remote_name, *remote_id)?;
term::success!("Remote {} added", term::format::tertiary(remote.name));
@ -179,5 +179,5 @@ pub fn setup_remote(
term::format::tertiary(term::format::node(remote_id))
);
}
Ok(())
Ok(remote_name)
}

View File

@ -528,7 +528,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if announce {
let mut node = Node::new(profile.socket());
node::sync(rid, &mut node)?;
node::announce(rid, &mut node)?;
}
Ok(())

View File

@ -106,7 +106,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if refs.is_none() && !options.all {
continue;
}
let seeded = policy.is_repo_seeded(&rid)?;
let seeded = policy.is_seeded(&rid)?;
if !seeded && !options.all {
continue;

View File

@ -794,7 +794,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if announce {
let mut node = Node::new(profile.socket());
node::sync(id, &mut node)?;
node::announce(id, &mut node)?;
}
Ok(())
}

View File

@ -39,6 +39,8 @@ List options
Add options
--name Override the name of the remote that by default is set to the node alias
--[no-]fetch Fetch the remote from local storage (default: fetch)
--[no-]sync Sync the remote refs from the network (default: sync)
Options
@ -56,9 +58,18 @@ pub enum OperationName {
#[derive(Debug)]
pub enum Operation {
Add { id: NodeId, name: Option<RefString> },
Rm { name: RefString },
List { option: ListOption },
Add {
id: NodeId,
name: Option<RefString>,
fetch: bool,
sync: bool,
},
Rm {
name: RefString,
},
List {
option: ListOption,
},
}
#[derive(Debug, Default)]
@ -83,6 +94,8 @@ impl Args for Options {
let mut id: Option<NodeId> = None;
let mut name: Option<RefString> = None;
let mut list_op: ListOption = ListOption::default();
let mut fetch = true;
let mut sync = true;
while let Some(arg) = parser.next()? {
match arg {
@ -114,6 +127,18 @@ impl Args for Options {
}
// Add options
Long("sync") if op == Some(OperationName::Add) => {
sync = true;
}
Long("no-sync") if op == Some(OperationName::Add) => {
sync = false;
}
Long("fetch") if op == Some(OperationName::Add) => {
fetch = true;
}
Long("no-fetch") if op == Some(OperationName::Add) => {
fetch = false;
}
Value(val) if op == Some(OperationName::Add) && id.is_none() => {
let nid = args::pubkey(&val)?;
id = Some(nid);
@ -137,6 +162,8 @@ impl Args for Options {
"`DID` required, try running `rad remote add <did>`"
))?,
name,
fetch,
sync,
},
OperationName::List => Operation::List { option: list_op },
OperationName::Rm => Operation::Rm {
@ -154,11 +181,25 @@ pub fn run(options: Options, ctx: impl Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
match options.op {
Operation::Add { ref id, name } => {
Operation::Add {
ref id,
name,
fetch,
sync,
} => {
let proj = profile.storage.repository(rid)?.project()?;
let branch = proj.default_branch();
self::add::run(rid, id, name, Some(branch.clone()), &profile, &working)?
self::add::run(
rid,
id,
name,
Some(branch.clone()),
&profile,
&working,
fetch,
sync,
)?
}
Operation::Rm { ref name } => self::rm::run(name, &working)?,
Operation::List { option } => match option {

View File

@ -1,10 +1,15 @@
use std::str::FromStr;
use std::time;
use radicle::git;
use radicle::git::RefString;
use radicle::prelude::*;
use radicle::Profile;
use radicle_crypto::PublicKey;
use crate::commands::rad_checkout as checkout;
use crate::git;
use crate::commands::rad_follow as follow;
use crate::commands::rad_sync as sync;
use crate::project::SetupRemote;
pub fn run(
@ -13,14 +18,33 @@ pub fn run(
name: Option<RefString>,
tracking: Option<BranchName>,
profile: &Profile,
repo: &git::Repository,
repo: &git::raw::Repository,
fetch: bool,
sync: bool,
) -> anyhow::Result<()> {
if sync {
let mut node = radicle::Node::new(profile.socket());
if !profile.policies()?.is_followed(nid)? {
let alias = name.as_ref().and_then(|n| Alias::from_str(n.as_str()).ok());
follow::follow(*nid, alias, &mut node, profile)?;
sync::fetch(
rid,
sync::RepoSync::default(),
time::Duration::from_secs(9),
&mut node,
)?;
}
}
let aliases = profile.aliases();
let setup = SetupRemote {
rid,
tracking,
fetch: false,
fetch,
repo,
};
checkout::setup_remote(&setup, nid, name, &aliases)
checkout::setup_remote(&setup, nid, name, &aliases)?;
Ok(())
}

View File

@ -280,7 +280,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}
Operation::Synchronize(SyncMode::Repo { mode, direction }) => {
if [SyncDirection::Fetch, SyncDirection::Both].contains(&direction) {
if !profile.policies()?.is_repo_seeded(&rid)? {
if !profile.policies()?.is_seeded(&rid)? {
anyhow::bail!("repository {rid} is not seeded");
}
let results = fetch(rid, mode.clone(), options.timeout, &mut node)?;

View File

@ -7,8 +7,9 @@ use radicle::Node;
use crate::terminal as term;
pub fn sync(rid: Id, node: &mut Node) -> anyhow::Result<()> {
match sync_(rid, node) {
/// Announce changes to the network.
pub fn announce(rid: Id, node: &mut Node) -> anyhow::Result<()> {
match announce_(rid, node) {
Ok(()) => Ok(()),
Err(e) if e.is_connection_err() => {
term::hint("Node is stopped. To announce changes to the network, start it with `rad node start`.");
@ -18,8 +19,7 @@ pub fn sync(rid: Id, node: &mut Node) -> anyhow::Result<()> {
}
}
/// Sync with the network.
fn sync_(rid: Id, node: &mut Node) -> Result<(), radicle::node::Error> {
fn announce_(rid: Id, node: &mut Node) -> Result<(), radicle::node::Error> {
let seeds = node.seeds(rid)?;
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();

View File

@ -204,7 +204,7 @@ impl Store<Write> {
/// `Config<Write>` can access these functions as well.
impl<T> Store<T> {
/// Check if a node is followed.
pub fn is_node_followed(&self, id: &NodeId) -> Result<bool, Error> {
pub fn is_followed(&self, id: &NodeId) -> Result<bool, Error> {
Ok(matches!(
self.follow_policy(id)?,
Some(Node {
@ -215,7 +215,7 @@ impl<T> Store<T> {
}
/// Check if a repository is seeded.
pub fn is_repo_seeded(&self, id: &Id) -> Result<bool, Error> {
pub fn is_seeded(&self, id: &Id) -> Result<bool, Error> {
Ok(matches!(
self.seed_policy(id)?,
Some(Repo {
@ -335,10 +335,10 @@ mod test {
let mut db = Store::open(":memory:").unwrap();
assert!(db.follow(&id, Some("eve")).unwrap());
assert!(db.is_node_followed(&id).unwrap());
assert!(db.is_followed(&id).unwrap());
assert!(!db.follow(&id, Some("eve")).unwrap());
assert!(db.unfollow(&id).unwrap());
assert!(!db.is_node_followed(&id).unwrap());
assert!(!db.is_followed(&id).unwrap());
}
#[test]
@ -347,10 +347,10 @@ mod test {
let mut db = Store::open(":memory:").unwrap();
assert!(db.seed(&id, Scope::All).unwrap());
assert!(db.is_repo_seeded(&id).unwrap());
assert!(db.is_seeded(&id).unwrap());
assert!(!db.seed(&id, Scope::All).unwrap());
assert!(db.unseed(&id).unwrap());
assert!(!db.is_repo_seeded(&id).unwrap());
assert!(!db.is_seeded(&id).unwrap());
}
#[test]

View File

@ -20,6 +20,7 @@ use thiserror::Error;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
use crate::crypto::{PublicKey, Signer};
use crate::node::policy::config::store::Read;
use crate::node::{policy, Alias, AliasStore};
use crate::prelude::Did;
use crate::prelude::{Id, NodeId};
@ -346,6 +347,17 @@ impl Profile {
&self.home
}
/// Return a read-only handle to the policies of the node.
pub fn policies(&self) -> Result<policy::config::Config<Read>, policy::store::Error> {
let path = self.node().join(node::POLICIES_DB_FILE);
let config = policy::config::Config::new(
self.config.node.policy,
self.config.node.scope,
policy::store::Store::reader(path)?,
);
Ok(config)
}
/// Return a multi-source store for aliases.
pub fn aliases(&self) -> Aliases {
let policies = self.home.policies().ok();
@ -471,15 +483,7 @@ impl Home {
.unwrap_or_else(|| self.node().join(node::DEFAULT_SOCKET_NAME))
}
/// Return a read-only handle to the policies of the node.
pub fn policies(&self) -> Result<policy::store::StoreReader, policy::store::Error> {
let path = self.node().join(node::POLICIES_DB_FILE);
let config = policy::store::Store::reader(path)?;
Ok(config)
}
/// Return a read-write handle to the policies of the node.
/// Return a read-write handle to the policies store of the node.
pub fn policies_mut(&self) -> Result<policy::store::StoreWriter, policy::store::Error> {
let path = self.node().join(node::POLICIES_DB_FILE);
let config = policy::store::Store::open(path)?;
@ -504,6 +508,17 @@ impl Home {
}
}
// Private methods.
impl Home {
/// Return a read-only handle to the policies store of the node.
fn policies(&self) -> Result<policy::store::StoreReader, policy::store::Error> {
let path = self.node().join(node::POLICIES_DB_FILE);
let config = policy::store::Store::reader(path)?;
Ok(config)
}
}
#[cfg(test)]
#[cfg(not(target_os = "macos"))]
mod test {