Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Fix inventory announcements for unseeded repositories
Merged did:key:z6MksFqX...wzpT opened 1 year ago

We were often not announcing the correct set of repositories. This could cause fetches to fail, and unseeded repositories to be advertized.

See commits for more details.

45 files changed +837 -552 6966c971 345ca923
modified radicle-cli/examples/rad-cob-log.md
@@ -7,7 +7,7 @@ $ rad issue open --title "flux capacitor underpowered" --description "Flux capac
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
-
│ Author  z6MknSL…StBU8Vi (you)                           │
+
│ Author  alice (you)                                     │
│ Status  open                                            │
│                                                         │
│ Flux capacitor power requirements exceed current supply │
@@ -18,11 +18,11 @@ The issue is now listed under our project.

```
$ rad issue list
-
╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●   ID        Title                         Author                    Labels   Assignees   Opened │
-
├───────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   d87dcfe   flux capacitor underpowered   z6MknSL…StBU8Vi   (you)                        now    │
-
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭──────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●   ID        Title                         Author           Labels   Assignees   Opened │
+
├──────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●   d87dcfe   flux capacitor underpowered   alice    (you)                        now    │
+
╰──────────────────────────────────────────────────────────────────────────────────────────╯
```

Let's create a patch, too.
@@ -42,11 +42,11 @@ Patch can be listed.

```
$ rad patch
-
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title                      Author                  Reviews  Head     +   -   Updated │
-
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  z6MknSL…StBU8Vi  (you)  -        3e674d1  +0  -0  now     │
-
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭─────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
+
├─────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  aa45913  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
+
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

Both issue and patch COBs can be listed.
modified radicle-cli/examples/rad-cob-show.md
@@ -10,7 +10,7 @@ $ rad issue open --title "spice harvester broken" --description "Fremen have att
╭──────────────────────────────────────────────────╮
│ Title   spice harvester broken                   │
│ Issue   9de644864342d7a505eb8d58d1ef20e5bb05de2e │
-
│ Author  z6MknSL…StBU8Vi (you)                    │
+
│ Author  alice (you)                              │
│ Status  open                                     │
│                                                  │
│ Fremen have attacked, maybe we went too far?     │
@@ -21,11 +21,11 @@ The issue is now listed under our project.

```
$ rad issue list
-
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●   ID        Title                    Author                    Labels   Assignees   Opened │
-
├──────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   9de6448   spice harvester broken   z6MknSL…StBU8Vi   (you)                        now    │
-
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭─────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●   ID        Title                    Author           Labels   Assignees   Opened │
+
├─────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●   9de6448   spice harvester broken   alice    (you)                        now    │
+
╰─────────────────────────────────────────────────────────────────────────────────────╯
```

Let's create a patch, too.
@@ -45,11 +45,11 @@ Patch can be listed.

```
$ rad patch
-
╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title                        Author                  Reviews  Head     +   -   Updated │
-
├────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  d1f7f86  Start drafting peace treaty  z6MknSL…StBU8Vi  (you)  -        575ed68  +0  -0  now     │
-
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭───────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                        Author         Reviews  Head     +   -   Updated │
+
├───────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  d1f7f86  Start drafting peace treaty  alice   (you)  -        575ed68  +0  -0  now     │
+
╰───────────────────────────────────────────────────────────────────────────────────────────╯
```

Both issue and patch COBs can be listed.
modified radicle-cli/examples/rad-id-update-delete-field.md
@@ -3,18 +3,18 @@ Let's add a payload field and then delete it.
```
$ rad id update --title "Add field" --description "Add a new 'web' field" --payload xyz.radicle.project web '"https://acme.example"'
✓ Identity revision a8a9fee6c4f83578ab132d375f1da0c81282bef3 created
-
╭───────────────────────────────────────────────────────────────────╮
-
│ Title    Add field                                                │
-
│ Revision a8a9fee6c4f83578ab132d375f1da0c81282bef3                 │
-
│ Blob     fbe268d13e60f1f3a1972e0ccd592f3cdecf08b5                 │
-
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
-
│ State    accepted                                                 │
-
│ Quorum   yes                                                      │
-
│                                                                   │
-
│ Add a new 'web' field                                             │
-
├───────────────────────────────────────────────────────────────────┤
-
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi  (you) │
-
╰───────────────────────────────────────────────────────────────────╯
+
╭────────────────────────────────────────────────────────────────────────╮
+
│ Title    Add field                                                     │
+
│ Revision a8a9fee6c4f83578ab132d375f1da0c81282bef3                      │
+
│ Blob     fbe268d13e60f1f3a1972e0ccd592f3cdecf08b5                      │
+
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
+
│ State    accepted                                                      │
+
│ Quorum   yes                                                           │
+
│                                                                        │
+
│ Add a new 'web' field                                                  │
+
├────────────────────────────────────────────────────────────────────────┤
+
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
+
╰────────────────────────────────────────────────────────────────────────╯

@@ -1,13 +1,14 @@
 {
@@ -39,18 +39,18 @@ Now let's delete it by setting it to `null`.
```
$ rad id update --title "Delete field" --description "Delete 'web'" --payload xyz.radicle.project web null
✓ Identity revision d373c35876833105f8aafed8b610660b5737cd67 created
-
╭───────────────────────────────────────────────────────────────────╮
-
│ Title    Delete field                                             │
-
│ Revision d373c35876833105f8aafed8b610660b5737cd67                 │
-
│ Blob     d96f425412c9f8ad5d9a9a05c9831d0728e2338d                 │
-
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
-
│ State    accepted                                                 │
-
│ Quorum   yes                                                      │
-
│                                                                   │
-
│ Delete 'web'                                                      │
-
├───────────────────────────────────────────────────────────────────┤
-
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi  (you) │
-
╰───────────────────────────────────────────────────────────────────╯
+
╭────────────────────────────────────────────────────────────────────────╮
+
│ Title    Delete field                                                  │
+
│ Revision d373c35876833105f8aafed8b610660b5737cd67                      │
+
│ Blob     d96f425412c9f8ad5d9a9a05c9831d0728e2338d                      │
+
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
+
│ State    accepted                                                      │
+
│ Quorum   yes                                                           │
+
│                                                                        │
+
│ Delete 'web'                                                           │
+
├────────────────────────────────────────────────────────────────────────┤
+
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
+
╰────────────────────────────────────────────────────────────────────────╯

@@ -1,14 +1,13 @@
 {
added radicle-cli/examples/rad-init-no-seed.md
@@ -0,0 +1,29 @@
+
If we initialize a public repository without seeding it, it won't be advertized:
+
```
+
$ rad init --name heartwood --description "radicle heartwood protocol & stack" --no-confirm --public --no-seed
+

+
Initializing public radicle 👾 repository in [..]
+

+
✓ Repository heartwood created.
+

+
Your Repository ID (RID) is rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK.
+
You can show it any time by running `rad .` from this directory.
+

+
Your repository will be announced to the network when you start your node.
+
You can start your node with `rad node start`.
+
To push changes, run `git push`.
+
```
+
```
+
$ rad node inventory
+
```
+

+
If we then seed it, it becomes advertized in our inventory:
+
```
+
$ rad seed rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
+
✓ Inventory updated with rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
+
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'all'
+
```
+
```
+
$ rad node inventory
+
rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
+
```
added radicle-cli/examples/rad-init-private-no-seed.md
@@ -0,0 +1,37 @@
+
Let's say we initialize a private repository and specify that we don't want it
+
to be seeded. This means that the repo will be available locally, to us,
+
and even if other peers know about it, they won't be able to fetch it
+
from us.
+
```
+
$ rad init --name heartwood --description "radicle heartwood protocol & stack" --no-confirm --private --no-seed
+

+
Initializing private radicle 👾 repository in [..]
+

+
✓ Repository heartwood created.
+

+
Your Repository ID (RID) is rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu.
+
You can show it any time by running `rad .` from this directory.
+

+
You have created a private repository.
+
This repository will only be visible to you, and to peers you explicitly allow.
+

+
To make it public, run `rad publish`.
+
To push changes, run `git push`.
+
```
+

+
```
+
$ rad seed
+
No seeding policies to show.
+
```
+

+
We can decide to seed it later, so that others can fetch it from us, given
+
that they are part of the allow list:
+
```
+
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu
+
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
+
```
+

+
But it still won't show up in our inventory, since it's private:
+
```
+
$ rad node inventory
+
```
modified radicle-cli/examples/rad-init-private.md
@@ -16,3 +16,15 @@ This repository will only be visible to you, and to peers you explicitly allow.
To make it public, run `rad publish`.
To push changes, run `git push`.
```
+

+
The repository does not show up in our inventory, since it is not advertized,
+
despite being seeded:
+
```
+
$ rad node inventory
+
$ rad seed
+
╭────────────────────────────────────────────────────────────────╮
+
│ Repository                          Name        Policy   Scope │
+
├────────────────────────────────────────────────────────────────┤
+
│ rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu   heartwood   allow    all   │
+
╰────────────────────────────────────────────────────────────────╯
+
```
modified radicle-cli/examples/rad-init.md
@@ -29,7 +29,7 @@ $ rad init
✗ Error: repository is already initialized with remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
```

-
Projects can be listed with the `ls` command:
+
Repositories can be listed with the `ls` command:

```
$ rad ls
@@ -39,3 +39,10 @@ $ rad ls
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```
+

+
Public repositories are added to our inventory:
+

+
```
+
$ rad node inventory
+
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
```
modified radicle-cli/examples/rad-inspect.md
@@ -48,7 +48,7 @@ $ rad inspect --payload
  }
}
$ rad inspect --delegates
-
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
```

Finally, the `--history` flag allows you to examine the identity document's
modified radicle-cli/examples/rad-issue.md
@@ -8,7 +8,7 @@ $ rad issue open --title "flux capacitor underpowered" --description "Flux capac
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
-
│ Author  z6MknSL…StBU8Vi (you)                           │
+
│ Author  alice (you)                                     │
│ Status  open                                            │
│                                                         │
│ Flux capacitor power requirements exceed current supply │
@@ -19,11 +19,11 @@ The issue is now listed under our project.

```
$ rad issue list
-
╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●   ID        Title                         Author                    Labels   Assignees   Opened │
-
├───────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   d87dcfe   flux capacitor underpowered   z6MknSL…StBU8Vi   (you)                        now    │
-
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭──────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●   ID        Title                         Author           Labels   Assignees   Opened │
+
├──────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●   d87dcfe   flux capacitor underpowered   alice    (you)                        now    │
+
╰──────────────────────────────────────────────────────────────────────────────────────────╯
```

Show the issue information issue.
@@ -33,7 +33,7 @@ $ rad issue show d87dcfe
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
-
│ Author  z6MknSL…StBU8Vi (you)                           │
+
│ Author  alice (you)                                     │
│ Status  open                                            │
│                                                         │
│ Flux capacitor power requirements exceed current supply │
@@ -59,11 +59,11 @@ It will now show in the list of issues assigned to us, along with the new label.

```
$ rad issue list --assigned
-
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●   ID        Title                         Author                    Labels             Assignees         Opened │
-
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   d87dcfe   flux capacitor underpowered   z6MknSL…StBU8Vi   (you)   good-first-issue   z6MknSL…StBU8Vi   now    │
-
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●   ID        Title                         Author           Labels             Assignees   Opened │
+
├────────────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●   d87dcfe   flux capacitor underpowered   alice    (you)   good-first-issue   alice       now    │
+
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

Note: this can always be undone with the `unassign` subcommand.
@@ -91,16 +91,16 @@ $ rad issue show d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
-
│ Author  z6MknSL…StBU8Vi (you)                           │
+
│ Author  alice (you)                                     │
│ Labels  good-first-issue                                │
│ Status  open                                            │
│                                                         │
│ Flux capacitor power requirements exceed current supply │
├─────────────────────────────────────────────────────────┤
-
│ z6MknSL…StBU8Vi (you) now 2193e87                       │
+
│ alice (you) now 2193e87                                 │
│ The flux capacitor needs 1.21 Gigawatts                 │
├─────────────────────────────────────────────────────────┤
-
│ z6MknSL…StBU8Vi (you) now 880fdcd                       │
+
│ alice (you) now 880fdcd                                 │
│ More power!                                             │
╰─────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-node.md
@@ -34,9 +34,9 @@ $ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias Bo
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (Bob)
```

-
Now, when we use the `rad seed` command we will see
-
information for repositories that we seed -- in this case a
-
repository that was already created:
+
Now, when we use the `rad seed` command we will see information for
+
repositories that we seed -- in this case a repository that was already
+
created:

```
$ rad seed
@@ -87,6 +87,32 @@ $ rad node stop
✗ Stopping node... error: node is not running
```

+
Note that if we unseed a repository, it is no longer part of our inventory:
+

+
```
+
$ rad unseed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
✓ Seeding policy for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji removed
+
$ rad node inventory
+
```
+

+
Likewise, if we seed a repository we don't have locally, it won't show up as
+
part of our inventory:
+
```
+
$ rad seed rad:z3trNYnLWS11cJWC6BbxDs5niGo82
+
[...]
+
$ rad node inventory
+
```
+

+
But if we start seeding the repository we have locally again, it'll show
+
up in our inventory:
+
```
+
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
✓ Inventory updated with rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
$ rad node inventory
+
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
```
+

Some commands also give us a hint if the node isn't running:

``` (fail)
modified radicle-cli/examples/rad-patch-ahead-behind.md
@@ -45,11 +45,11 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
When listing, we see that it has one addition:
```
$ rad patch list
-
╭─────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title     Author                  Reviews  Head     +   -   Updated │
-
├─────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  217f050  Add Alan  z6MknSL…StBU8Vi  (you)  -        5c88a79  +1  -0  now     │
-
╰─────────────────────────────────────────────────────────────────────────────────╯
+
╭────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title     Author         Reviews  Head     +   -   Updated │
+
├────────────────────────────────────────────────────────────────────────┤
+
│ ●  217f050  Add Alan  alice   (you)  -        5c88a79  +1  -0  now     │
+
╰────────────────────────────────────────────────────────────────────────╯
```

When showing the patch, we see that it is `ahead 1, behind 1`, since master has
@@ -59,7 +59,7 @@ $ rad patch show -v -p 217f050
╭────────────────────────────────────────────────────╮
│ Title     Add Alan                                 │
│ Patch     217f050f8891def8fb863f7c0b4f85c89f97299d │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │
│ Base      f64fb2c8fe28f7c458c72ec8d700373924794943 │
│ Branches  feature/1                                │
@@ -68,7 +68,7 @@ $ rad patch show -v -p 217f050
├────────────────────────────────────────────────────┤
│ 5c88a79 Add Alan                                   │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (5c88a79) now    │
+
│ ● opened by alice (you) (5c88a79) now              │
╰────────────────────────────────────────────────────╯

commit 5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7
@@ -105,7 +105,7 @@ $ rad patch show -v e22ff008e2a0ed47262890d13263031d7555b555
╭────────────────────────────────────────────────────╮
│ Title     Add Mel                                  │
│ Patch     e22ff008e2a0ed47262890d13263031d7555b555 │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
│ Base      f64fb2c8fe28f7c458c72ec8d700373924794943 │
│ Branches  feature/2                                │
@@ -115,7 +115,7 @@ $ rad patch show -v e22ff008e2a0ed47262890d13263031d7555b555
│ 7f63fcb Add Mel                                    │
│ 5c88a79 Add Alan                                   │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (7f63fcb) now    │
+
│ ● opened by alice (you) (7f63fcb) now              │
╰────────────────────────────────────────────────────╯
```

@@ -140,7 +140,7 @@ $ rad patch show -v a467ffa260c4fbe355b6fb550ba0c4956078717e
╭────────────────────────────────────────────────────╮
│ Title     Add Mel #2                               │
│ Patch     a467ffa260c4fbe355b6fb550ba0c4956078717e │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
│ Base      5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │
│ Branches  feature/2                                │
@@ -149,6 +149,6 @@ $ rad patch show -v a467ffa260c4fbe355b6fb550ba0c4956078717e
├────────────────────────────────────────────────────┤
│ 7f63fcb Add Mel                                    │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (7f63fcb) now    │
+
│ ● opened by alice (you) (7f63fcb) now              │
╰────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-patch-change-base.md
@@ -47,7 +47,7 @@ $ rad patch show 183d343ab47d7fe18baf1b24b7209ad033d7fe5c -v
╭────────────────────────────────────────────────────╮
│ Title     Add README, just for the fun             │
│ Patch     183d343ab47d7fe18baf1b24b7209ad033d7fe5c │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
│ Base      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
│ Branches  add-readme                               │
@@ -57,7 +57,7 @@ $ rad patch show 183d343ab47d7fe18baf1b24b7209ad033d7fe5c -v
│ 27857ec Add README, just for the fun               │
│ 3e674d1 Define power requirements                  │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (27857ec) now    │
+
│ ● opened by alice (you) (27857ec) now              │
╰────────────────────────────────────────────────────╯
```

@@ -78,7 +78,7 @@ $ rad patch show 183d343 -v
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add README, just for the fun                              │
│ Patch     183d343ab47d7fe18baf1b24b7209ad033d7fe5c                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66                  │
│ Base      3e674d1a1df90807e934f9ae5da2591dd6848a33                  │
│ Branches  add-readme                                                │
@@ -87,7 +87,7 @@ $ rad patch show 183d343 -v
├─────────────────────────────────────────────────────────────────────┤
│ 27857ec Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (27857ec) now                     │
+
│ ● opened by alice (you) (27857ec) now                               │
│ ↑ updated to ebe76f9c2148eb595d7a745f82275786bf3458c3 (27857ec) now │
╰─────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-patch-checkout-revision.md
@@ -18,7 +18,7 @@ $ rad patch show aa45913
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Define power requirements                                 │
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      639f44a25145a37f747f3c84265037a9461e44c5                  │
│ Branches  patch/aa45913                                             │
│ Commits   ahead 3, behind 0                                         │
@@ -30,7 +30,7 @@ $ rad patch show aa45913
│ 27857ec Add README, just for the fun                                │
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (3e674d1) now                     │
+
│ ● opened by alice (you) (3e674d1) now                               │
│ ↑ updated to 3156bed9d64d4675d6cf56612d217fc5f4e8a53a (27857ec) now │
│ ↑ updated to 2f5324f61e05cda65b667eeea02570d077a8e724 (639f44a) now │
╰─────────────────────────────────────────────────────────────────────╯
modified radicle-cli/examples/rad-patch-draft.md
@@ -21,7 +21,7 @@ $ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
╭────────────────────────────────────────────────────╮
│ Title     Nothing yet                              │
│ Patch     97e18f8598237a396a1c0ac1509c89028e666c97 │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches  cloudhead/draft                          │
│ Commits   ahead 1, behind 0                        │
@@ -29,7 +29,7 @@ $ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
├────────────────────────────────────────────────────┤
│ 2a46583 Nothing to see here..                      │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (2a46583) [ .. ] │
+
│ ● opened by alice (you) (2a46583) [ .. ]           │
╰────────────────────────────────────────────────────╯
```

@@ -44,7 +44,7 @@ $ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
╭────────────────────────────────────────────────────╮
│ Title     Nothing yet                              │
│ Patch     97e18f8598237a396a1c0ac1509c89028e666c97 │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches  cloudhead/draft                          │
│ Commits   ahead 1, behind 0                        │
@@ -52,7 +52,7 @@ $ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
├────────────────────────────────────────────────────┤
│ 2a46583 Nothing to see here..                      │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (2a46583) [ .. ] │
+
│ ● opened by alice (you) (2a46583) [ .. ]           │
╰────────────────────────────────────────────────────╯
```

@@ -65,7 +65,7 @@ $ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
╭────────────────────────────────────────────────────╮
│ Title     Nothing yet                              │
│ Patch     97e18f8598237a396a1c0ac1509c89028e666c97 │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches  cloudhead/draft                          │
│ Commits   ahead 1, behind 0                        │
@@ -73,6 +73,6 @@ $ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
├────────────────────────────────────────────────────┤
│ 2a46583 Nothing to see here..                      │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (2a46583) [ .. ] │
+
│ ● opened by alice (you) (2a46583) [ .. ]           │
╰────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-patch-edit.md
@@ -44,7 +44,7 @@ $ rad patch show 89f7afb
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add README, just for the fun                              │
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
│ Commits   ahead 2, behind 0                                         │
@@ -53,7 +53,7 @@ $ rad patch show 89f7afb
│ 8945f61 Define the LICENSE                                          │
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (03c02af) now                     │
+
│ ● opened by alice (you) (03c02af) now                               │
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```
@@ -67,7 +67,7 @@ $ rad patch show 89f7afb
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add Metadata                                              │
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
│ Commits   ahead 2, behind 0                                         │
@@ -78,7 +78,7 @@ $ rad patch show 89f7afb
│ 8945f61 Define the LICENSE                                          │
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (03c02af) now                     │
+
│ ● opened by alice (you) (03c02af) now                               │
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```
@@ -95,7 +95,7 @@ $ rad patch show 89f7afb
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add Metadata                                              │
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
│ Commits   ahead 2, behind 0                                         │
@@ -106,7 +106,7 @@ $ rad patch show 89f7afb
│ 8945f61 Define the LICENSE                                          │
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (03c02af) now                     │
+
│ ● opened by alice (you) (03c02af) now                               │
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-patch-update.md
@@ -16,7 +16,7 @@ $ rad patch show b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5
╭────────────────────────────────────────────────────╮
│ Title     Not a real change                        │
│ Patch     b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5 │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      51b2f0f77b9849bfaa3e9d3ff68ee2f57771d20c │
│ Branches  feature/1                                │
│ Commits   ahead 1, behind 0                        │
@@ -24,7 +24,7 @@ $ rad patch show b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5
├────────────────────────────────────────────────────┤
│ 51b2f0f Not a real change                          │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (51b2f0f) now    │
+
│ ● opened by alice (you) (51b2f0f) now              │
╰────────────────────────────────────────────────────╯
```

@@ -57,7 +57,7 @@ $ rad patch show b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Not a real change                                         │
│ Patch     b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      4d272148458a17620541555b1f0905c01658aa9f                  │
│ Branches  feature/1                                                 │
│ Commits   ahead 2, behind 0                                         │
@@ -66,7 +66,7 @@ $ rad patch show b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5
│ 4d27214 Rename readme file                                          │
│ 51b2f0f Not a real change                                           │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (51b2f0f) now                     │
+
│ ● opened by alice (you) (51b2f0f) now                               │
│ ↑ updated to ea7def3857f62f404606d7cd6490cd0de4eaebd1 (4d27214) now │
╰─────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-patch-via-push.md
@@ -23,7 +23,7 @@ $ rad patch show 6035d2f582afbe01ff23ea87528ae523d76875b6
╭────────────────────────────────────────────────────╮
│ Title     Add things #1                            │
│ Patch     6035d2f582afbe01ff23ea87528ae523d76875b6 │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      42d894a83c9c356552a57af09ccdbd5587a99045 │
│ Branches  feature/1                                │
│ Commits   ahead 1, behind 0                        │
@@ -33,7 +33,7 @@ $ rad patch show 6035d2f582afbe01ff23ea87528ae523d76875b6
├────────────────────────────────────────────────────┤
│ 42d894a Add things                                 │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (42d894a) now    │
+
│ ● opened by alice (you) (42d894a) now              │
╰────────────────────────────────────────────────────╯
```

@@ -96,12 +96,12 @@ And both patches:

```
$ rad patch
-
╭────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title            Author                  Reviews  Head     +   -   Updated │
-
├────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  6035d2f  Add things #1    z6MknSL…StBU8Vi  (you)  -        42d894a  +0  -0  now     │
-
│ ●  9580891  Add more things  z6MknSL…StBU8Vi  (you)  -        8b0ea80  +0  -0  now     │
-
╰────────────────────────────────────────────────────────────────────────────────────────╯
+
╭───────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title            Author         Reviews  Head     +   -   Updated │
+
├───────────────────────────────────────────────────────────────────────────────┤
+
│ ●  6035d2f  Add things #1    alice   (you)  -        42d894a  +0  -0  now     │
+
│ ●  9580891  Add more things  alice   (you)  -        8b0ea80  +0  -0  now     │
+
╰───────────────────────────────────────────────────────────────────────────────╯
```

To update our patch, we simply push commits to the upstream branch:
@@ -136,7 +136,7 @@ $ rad patch show 9580891
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add more things                                           │
│ Patch     95808913573cead52ad7b42c7b475260ec45c4b2                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      02bef3fac41b2f98bb3c02b868a53ddfecb55b5f                  │
│ Branches  feature/2                                                 │
│ Commits   ahead 2, behind 0                                         │
@@ -145,7 +145,7 @@ $ rad patch show 9580891
│ 02bef3f Improve code                                                │
│ 8b0ea80 Add more things                                             │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (8b0ea80) now                     │
+
│ ● opened by alice (you) (8b0ea80) now                               │
│ ↑ updated to d7040c6c97629c2b94f86fb639bebbff5de39697 (02bef3f) now │
╰─────────────────────────────────────────────────────────────────────╯
```
@@ -212,7 +212,7 @@ $ rad patch show 9580891
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add more things                                           │
│ Patch     95808913573cead52ad7b42c7b475260ec45c4b2                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Head      9304dbc445925187994a7a93222a3f8bde73b785                  │
│ Branches  feature/2                                                 │
│ Commits   ahead 2, behind 0                                         │
@@ -221,7 +221,7 @@ $ rad patch show 9580891
│ 9304dbc Amended commit                                              │
│ 8b0ea80 Add more things                                             │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (8b0ea80) now                     │
+
│ ● opened by alice (you) (8b0ea80) now                               │
│ ↑ updated to d7040c6c97629c2b94f86fb639bebbff5de39697 (02bef3f) now │
│ ↑ updated to 670d02794aa05afd6e0851f4aa848bc87c4712c7 (9304dbc) now │
╰─────────────────────────────────────────────────────────────────────╯
modified radicle-cli/examples/rad-patch.md
@@ -35,18 +35,18 @@ It will now be listed as one of the project's open patches.

```
$ rad patch
-
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title                      Author                  Reviews  Head     +   -   Updated │
-
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  z6MknSL…StBU8Vi  (you)  -        3e674d1  +0  -0  now     │
-
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭─────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
+
├─────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  aa45913  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
+
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```
```
$ rad patch show aa45913e757cacd46972733bddee5472c78fa32a -p
╭────────────────────────────────────────────────────╮
│ Title     Define power requirements                │
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Head      3e674d1a1df90807e934f9ae5da2591dd6848a33 │
│ Branches  flux-capacitor-power                     │
│ Commits   ahead 1, behind 0                        │
@@ -56,7 +56,7 @@ $ rad patch show aa45913e757cacd46972733bddee5472c78fa32a -p
├────────────────────────────────────────────────────┤
│ 3e674d1 Define power requirements                  │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (3e674d1) now    │
+
│ ● opened by alice (you) (3e674d1) now              │
╰────────────────────────────────────────────────────╯

commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
@@ -75,11 +75,11 @@ We can also list only patches that we've authored.

```
$ rad patch list --authored
-
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title                      Author                  Reviews  Head     +   -   Updated │
-
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  z6MknSL…StBU8Vi  (you)  -        3e674d1  +0  -0  now     │
-
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭─────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
+
├─────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  aa45913  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
+
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

We can also see that it set an upstream for our patch branch:
@@ -99,7 +99,7 @@ $ rad patch show aa45913
╭────────────────────────────────────────────────────╮
│ Title     Define power requirements                │
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a │
-
│ Author    z6MknSL…StBU8Vi (you)                    │
+
│ Author    alice (you)                              │
│ Labels    fun                                      │
│ Head      3e674d1a1df90807e934f9ae5da2591dd6848a33 │
│ Branches  flux-capacitor-power                     │
@@ -110,7 +110,7 @@ $ rad patch show aa45913
├────────────────────────────────────────────────────┤
│ 3e674d1 Define power requirements                  │
├────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (3e674d1) now    │
+
│ ● opened by alice (you) (3e674d1) now              │
╰────────────────────────────────────────────────────╯
```

@@ -136,7 +136,7 @@ And let's leave a quick comment for our team:
```
$ rad patch comment aa45913 --message 'I cannot wait to get back to the 90s!' --no-announce
╭───────────────────────────────────────╮
-
│ z6MknSL…StBU8Vi (you) now 686ec1c     │
+
│ alice (you) now 686ec1c               │
│ I cannot wait to get back to the 90s! │
╰───────────────────────────────────────╯
$ rad patch comment aa45913 --message 'My favorite decade!' --reply-to 686ec1c -q --no-announce
@@ -165,7 +165,7 @@ $ rad patch show aa45913
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Define power requirements                                 │
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Labels    fun                                                       │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66                  │
│ Branches  flux-capacitor-power, patch/aa45913                       │
@@ -177,16 +177,16 @@ $ rad patch show aa45913
│ 27857ec Add README, just for the fun                                │
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (3e674d1) now                     │
+
│ ● opened by alice (you) (3e674d1) now                               │
│ ↑ updated to 6e5a3b7b2ce27b32e7ccc2f0b3f4594897dde638 (27857ec) now │
-
│   └─ ✓ accepted by z6MknSL…StBU8Vi (you) now                        │
+
│   └─ ✓ accepted by alice (you) now                                  │
╰─────────────────────────────────────────────────────────────────────╯
$ rad patch list
-
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title                      Author                  Reviews  Head     +   -   Updated │
-
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  z6MknSL…StBU8Vi  (you)  ✔        27857ec  +0  -0  now     │
-
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
+
╭─────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
+
├─────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  aa45913  Define power requirements  alice   (you)  ✔        27857ec  +0  -0  now     │
+
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

If you make a mistake on the patch description, you can always change it!
@@ -197,7 +197,7 @@ $ rad patch show aa45913
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Define power requirements                                 │
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a                  │
-
│ Author    z6MknSL…StBU8Vi (you)                                     │
+
│ Author    alice (you)                                               │
│ Labels    fun                                                       │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66                  │
│ Branches  flux-capacitor-power, patch/aa45913                       │
@@ -209,8 +209,8 @@ $ rad patch show aa45913
│ 27857ec Add README, just for the fun                                │
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
-
│ ● opened by z6MknSL…StBU8Vi (you) (3e674d1) now                     │
+
│ ● opened by alice (you) (3e674d1) now                               │
│ ↑ updated to 6e5a3b7b2ce27b32e7ccc2f0b3f4594897dde638 (27857ec) now │
-
│   └─ ✓ accepted by z6MknSL…StBU8Vi (you) now                        │
+
│   └─ ✓ accepted by alice (you) now                                  │
╰─────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-publish.md
@@ -3,13 +3,21 @@ Let's say we have a private repo. To make it public, we use the `publish` comman
```
$ rad inspect --visibility
private
+
$ rad node inventory
$ rad publish
+
✓ Updating inventory..
✓ Repository is now public
! Warning: Your node is not running. Start your node with `rad node start` to announce your repository to the network
$ rad inspect --visibility
public
```

+
The repository is now in our inventory:
+
```
+
$ rad node inventory
+
rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu
+
```
+

If we try to publish again, we get an error:

``` (fail)
@@ -27,18 +35,18 @@ repository private again __will not_ be replicated.
```
$ rad id update --visibility private --title "Privatise" --description "Reverting the rad publish event"
✓ Identity revision 774cc1e72641d97d7dc9377745b7f454a9171747 created
-
╭───────────────────────────────────────────────────────────────────╮
-
│ Title    Privatise                                                │
-
│ Revision 774cc1e72641d97d7dc9377745b7f454a9171747                 │
-
│ Blob     88f759a4d46e9535766fccec0cbfe1fed6160b1a                 │
-
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
-
│ State    accepted                                                 │
-
│ Quorum   yes                                                      │
-
│                                                                   │
-
│ Reverting the rad publish event                                   │
-
├───────────────────────────────────────────────────────────────────┤
-
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi  (you) │
-
╰───────────────────────────────────────────────────────────────────╯
+
╭────────────────────────────────────────────────────────────────────────╮
+
│ Title    Privatise                                                     │
+
│ Revision 774cc1e72641d97d7dc9377745b7f454a9171747                      │
+
│ Blob     88f759a4d46e9535766fccec0cbfe1fed6160b1a                      │
+
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
+
│ State    accepted                                                      │
+
│ Quorum   yes                                                           │
+
│                                                                        │
+
│ Reverting the rad publish event                                        │
+
├────────────────────────────────────────────────────────────────────────┤
+
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
+
╰────────────────────────────────────────────────────────────────────────╯

@@ -1,13 +1,16 @@
 {
modified radicle-cli/examples/rad-unseed.md
@@ -35,3 +35,11 @@ $ rad ls --all
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   local        f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```
+

+
Hence, we also see that it isn't in our inventory and isn't seeded:
+

+
```
+
$ rad node inventory
+
$ rad seed
+
No seeding policies to show.
+
```
modified radicle-cli/src/commands/init.rs
@@ -20,7 +20,6 @@ use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT};
use radicle::prelude::Doc;
use radicle::{profile, Node};

-
use crate as cli;
use crate::commands;
use crate::git;
use crate::terminal as term;
@@ -287,11 +286,14 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
            if options.verbose {
                term::blob(json::to_string_pretty(&proj)?);
            }
-

            // It's important to seed our own repositories to make sure that our node signals
            // interest for them. This ensures that messages relating to them are relayed to us.
            if options.seed {
-
                cli::project::seed(rid, options.scope, &mut node, profile)?;
+
                profile.seed(rid, options.scope, &mut node)?;
+

+
                if doc.visibility.is_public() {
+
                    profile.add_inventory(rid, &mut node)?;
+
                }
            }

            if options.set_upstream || git::branch_remote(&repo, proj.default_branch()).is_err() {
@@ -370,7 +372,6 @@ fn sync(
    let events = node.subscribe(DEFAULT_SUBSCRIBE_TIMEOUT)?;
    let sessions = node.sessions()?;

-
    node.update_inventory(rid)?;
    spinner.message("Announcing..");

    if !sessions.iter().any(|s| s.is_connected()) {
modified radicle-cli/src/commands/node.rs
@@ -5,6 +5,7 @@ use std::time;
use anyhow::anyhow;

use radicle::node::config::ConnectAddress;
+
use radicle::node::routing::Store;
use radicle::node::Handle as _;
use radicle::node::{Address, Node, NodeId, PeerAddr};
use radicle::prelude::RepoId;
@@ -36,6 +37,7 @@ Usage
    rad node debug [<option>...]
    rad node connect <nid>@<addr> [<option>...]
    rad node routing [--rid <rid>] [--nid <nid>] [--json] [<option>...]
+
    rad node inventory [<option>...]
    rad node events [--timeout <secs>] [-n <count>] [<option>...]
    rad node config [--addresses]
    rad node db <command> [<option>..]
@@ -99,6 +101,7 @@ pub enum Operation {
        lines: usize,
    },
    Status,
+
    Inventory,
    Debug,
    Sessions,
    Stop,
@@ -115,6 +118,7 @@ pub enum OperationName {
    Start,
    #[default]
    Status,
+
    Inventory,
    Debug,
    Sessions,
    Stop,
@@ -151,6 +155,7 @@ impl Args for Options {
                    "logs" => op = Some(OperationName::Logs),
                    "config" => op = Some(OperationName::Config),
                    "routing" => op = Some(OperationName::Routing),
+
                    "inventory" => op = Some(OperationName::Inventory),
                    "start" => op = Some(OperationName::Start),
                    "status" => op = Some(OperationName::Status),
                    "stop" => op = Some(OperationName::Stop),
@@ -225,6 +230,7 @@ impl Args for Options {
                options,
                path: path.unwrap_or(PathBuf::from("radicle-node")),
            },
+
            OperationName::Inventory => Operation::Inventory,
            OperationName::Status => Operation::Status,
            OperationName::Debug => Operation::Debug,
            OperationName::Sessions => Operation::Sessions,
@@ -280,6 +286,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        } => {
            control::start(node, !foreground, verbose, options, &path, &profile)?;
        }
+
        Operation::Inventory => {
+
            for rid in profile.routing()?.get_inventory(profile.id())? {
+
                println!("{}", term::format::tertiary(rid));
+
            }
+
        }
        Operation::Status => {
            control::status(&node, &profile)?;
        }
modified radicle-cli/src/commands/node/routing.rs
@@ -27,7 +27,7 @@ pub fn run<S: node::routing::Store>(

fn print_table(entries: impl IntoIterator<Item = (RepoId, NodeId)>) {
    let mut t = term::Table::new(term::table::TableOptions::bordered());
-
    t.push([
+
    t.header([
        term::format::default(String::from("RID")),
        term::format::default(String::from("NID")),
    ]);
modified radicle-cli/src/commands/publish.rs
@@ -114,18 +114,19 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        }
        anyhow::bail!("fatal: repository storage is corrupt");
    }
+
    let mut node = radicle::Node::new(profile.socket());
+
    let spinner = term::spinner("Updating inventory..");
+

+
    // The repository is now part of our inventory.
+
    profile.add_inventory(rid, &mut node)?;
+
    spinner.finish();

    term::success!(
        "Repository is now {}",
        term::format::visibility(&doc.visibility)
    );

-
    let mut node = radicle::Node::new(profile.socket());
-
    if node.is_running() {
-
        let spinner = term::spinner("Announcing to network..");
-
        node.announce_inventory()?;
-
        spinner.finish();
-
    } else {
+
    if !node.is_running() {
        term::warning(format!(
            "Your node is not running. Start your node with {} to announce your repository \
            to the network",
modified radicle-cli/src/commands/seed.rs
@@ -10,8 +10,8 @@ use radicle_term::Element as _;

use crate::commands::rad_sync as sync;
use crate::node::SyncSettings;
+
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
-
use crate::{project, terminal as term};

pub const HELP: Help = Help {
    name: "seed",
@@ -134,9 +134,16 @@ pub fn update(
    node: &mut Node,
    profile: &Profile,
) -> Result<(), anyhow::Error> {
-
    let updated = project::seed(rid, scope, node, profile)?;
+
    let updated = profile.seed(rid, scope, node)?;
    let outcome = if updated { "updated" } else { "exists" };

+
    if let Ok(repo) = profile.storage.repository(rid) {
+
        if repo.identity_doc()?.visibility.is_public() {
+
            profile.add_inventory(rid, node)?;
+
            term::success!("Inventory updated with {}", term::format::tertiary(rid));
+
        }
+
    }
+

    term::success!(
        "Seeding policy {outcome} for {} with scope '{scope}'",
        term::format::tertiary(rid),
@@ -145,17 +152,11 @@ pub fn update(
    Ok(())
}

-
pub fn delete(rid: RepoId, node: &mut Node, profile: &Profile) -> anyhow::Result<()> {
-
    if project::unseed(rid, node, profile)? {
-
        term::success!("Seeding policy for {} removed", term::format::tertiary(rid));
-
    }
-
    Ok(())
-
}
-

pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
    let store = profile.policies()?;
    let storage = &profile.storage;
    let mut t = term::Table::new(term::table::TableOptions::bordered());
+

    t.header([
        term::format::default(String::from("Repository")),
        term::format::default(String::from("Name")),
modified radicle-cli/src/commands/unseed.rs
@@ -4,8 +4,8 @@ use anyhow::anyhow;

use radicle::{prelude::*, Node};

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

pub const HELP: Help = Help {
    name: "unseed",
@@ -72,7 +72,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}

pub fn delete(rid: RepoId, node: &mut Node, profile: &Profile) -> anyhow::Result<()> {
-
    if project::unseed(rid, node, profile)? {
+
    if profile.unseed(rid, node)? {
        term::success!("Seeding policy for {} removed", term::format::tertiary(rid));
    }
    Ok(())
modified radicle-cli/src/project.rs
@@ -2,9 +2,7 @@ use radicle::prelude::*;

use crate::git;
use radicle::git::RefStr;
-
use radicle::node::policy::Scope;
-
use radicle::node::{Handle, NodeId};
-
use radicle::Node;
+
use radicle::node::NodeId;

/// Setup a repository remote and tracking branch.
pub struct SetupRemote<'a> {
@@ -51,34 +49,3 @@ impl<'a> SetupRemote<'a> {
        Ok((remote, None))
    }
}
-

-
/// Seed a repository by first trying to seed through the node, and if the node isn't running,
-
/// by updating the policy database directly.
-
pub fn seed(
-
    rid: RepoId,
-
    scope: Scope,
-
    node: &mut Node,
-
    profile: &Profile,
-
) -> Result<bool, anyhow::Error> {
-
    match node.seed(rid, scope) {
-
        Ok(updated) => Ok(updated),
-
        Err(e) if e.is_connection_err() => {
-
            let mut config = profile.policies_mut()?;
-
            config.seed(&rid, scope).map_err(|e| e.into())
-
        }
-
        Err(e) => Err(e.into()),
-
    }
-
}
-

-
/// Unseed a repository by first trying to unseed through the node, and if the node isn't running,
-
/// by updating the policy database directly.
-
pub fn unseed(rid: RepoId, node: &mut Node, profile: &Profile) -> Result<bool, anyhow::Error> {
-
    match node.unseed(rid) {
-
        Ok(updated) => Ok(updated),
-
        Err(e) if e.is_connection_err() => {
-
            let mut config = profile.policies_mut()?;
-
            config.unseed(&rid).map_err(|e| e.into())
-
        }
-
        Err(e) => Err(e.into()),
-
    }
-
}
modified radicle-cli/tests/commands.rs
@@ -202,6 +202,23 @@ fn rad_init() {
}

#[test]
+
fn rad_init_no_seed() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node(Config::test(Alias::new("alice")));
+
    let working = environment.tmp().join("working");
+

+
    fixtures::repository(working.join("alice"));
+

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

+
#[test]
fn rad_init_with_existing_remote() {
    let mut environment = Environment::new();
    let profile = environment.profile(config::profile("alice"));
@@ -730,6 +747,7 @@ fn rad_node() {
            Address::from(net::SocketAddr::from(([41, 12, 98, 112], 8776))),
            Address::from_str("seed.cloudhead.io:8776").unwrap(),
        ],
+
        seeding_policy: SeedingPolicy::Block,
        ..Config::test(Alias::new("alice"))
    });
    let working = tempfile::tempdir().unwrap();
@@ -1286,6 +1304,8 @@ fn rad_clone_partial_fail() {
    let working = environment.tmp().join("working");
    let carol = NodeId::from_str("z6MksFqXN3Yhqk8pTJdUGLwBTkRfQvwZXPqR2qMEhbS9wzpT").unwrap();

+
    logger::init(log::Level::Debug);
+

    // Setup a test project.
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");

@@ -1310,7 +1330,7 @@ fn rad_clone_partial_fail() {
        .unwrap();
    eve.db
        .routing_mut()
-
        .insert([&acme], carol, localtime::LocalTime::now().into())
+
        .add_inventory([&acme], carol, localtime::LocalTime::now().into())
        .unwrap();
    eve.config.peers = node::config::PeerConfig::Static;

@@ -1323,7 +1343,8 @@ fn rad_clone_partial_fail() {
    eve.connect(&alice);
    eve.connect(&bob);
    eve.routes_to(&[(acme, carol), (acme, bob.id), (acme, alice.id)]);
-
    bob.handle.unseed(acme).unwrap(); // Cause the fetch with bob to fail.
+
    bob.storage.repository(acme).unwrap().remove().unwrap(); // Cause the fetch from Bob to fail.
+
    bob.storage.lock_repository(acme).ok(); // Prevent repo from being re-fetched.

    test(
        "examples/rad-clone-partial-fail.md",
@@ -1386,8 +1407,14 @@ fn rad_clone_connect() {
            )],
        )
        .unwrap();
-
    eve.db.routing_mut().insert([&acme], alice.id, now).unwrap();
-
    eve.db.routing_mut().insert([&acme], bob.id, now).unwrap();
+
    eve.db
+
        .routing_mut()
+
        .add_inventory([&acme], alice.id, now)
+
        .unwrap();
+
    eve.db
+
        .routing_mut()
+
        .add_inventory([&acme], bob.id, now)
+
        .unwrap();
    eve.config.peers = node::config::PeerConfig::Static;

    let eve = eve.spawn();
@@ -2159,6 +2186,23 @@ fn rad_init_private() {
}

#[test]
+
fn rad_init_private_no_seed() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node(Config::test(Alias::new("alice")));
+
    let working = environment.tmp().join("working");
+

+
    fixtures::repository(working.join("alice"));
+

+
    test(
+
        "examples/rad-init-private-no-seed.md",
+
        working.join("alice"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
fn rad_init_private_seed() {
    let mut environment = Environment::new();
    let alice = environment.node(Config::test(Alias::new("alice")));
modified radicle-node/src/control.rs
@@ -174,7 +174,7 @@ where
            }
            CommandResult::ok().to_writer(writer).ok();
        }
-
        Command::UpdateInventory { rid } => match handle.update_inventory(rid) {
+
        Command::AddInventory { rid } => match handle.add_inventory(rid) {
            Ok(result) => {
                CommandResult::updated(result).to_writer(writer)?;
            }
modified radicle-node/src/runtime.rs
@@ -125,10 +125,6 @@ impl Runtime {
            log::warn!(target: "node", "Unused or deprecated configuration attribute {:?}", key);
        }
        log::info!(target: "node", "Opening node database..");
-
        let db = home
-
            .database_mut()?
-
            .journal_mode(node::db::JournalMode::default())?;
-
        let mut stores: service::Stores<_> = db.clone().into();

        log::info!(target: "node", "Opening policy database..");
        let policies = home.policies_mut()?;
@@ -175,6 +171,18 @@ impl Runtime {
                .expect("Runtime::init: unable to solve proof-of-work puzzle")
        };

+
        let db = home
+
            .database_mut()?
+
            .journal_mode(node::db::JournalMode::default())?
+
            .init(
+
                &id,
+
                announcement.features,
+
                announcement.alias.clone(),
+
                announcement.timestamp,
+
                announcement.addresses.iter(),
+
            )?;
+
        let mut stores: service::Stores<_> = db.clone().into();
+

        if config.connect.is_empty() && stores.addresses().is_empty()? {
            log::info!(target: "node", "Address book is empty. Adding bootstrap nodes..");

modified radicle-node/src/runtime/handle.rs
@@ -260,9 +260,9 @@ impl radicle::node::Handle for Handle {
            .map_err(Error::from)
    }

-
    fn update_inventory(&mut self, rid: RepoId) -> Result<bool, Error> {
+
    fn add_inventory(&mut self, rid: RepoId) -> Result<bool, Error> {
        let (sender, receiver) = chan::bounded(1);
-
        self.command(service::Command::UpdateInventory(rid, sender))?;
+
        self.command(service::Command::AddInventory(rid, sender))?;
        receiver.recv().map_err(Error::from)
    }

modified radicle-node/src/service.rs
@@ -10,7 +10,7 @@ pub mod message;
pub mod session;

use std::collections::hash_map::Entry;
-
use std::collections::{BTreeSet, HashMap, VecDeque};
+
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
use std::net::IpAddr;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
@@ -216,8 +216,8 @@ pub enum Command {
    AnnounceRefs(RepoId, chan::Sender<RefsAt>),
    /// Announce local repositories to peers.
    AnnounceInventory,
-
    /// Update local inventory.
-
    UpdateInventory(RepoId, chan::Sender<bool>),
+
    /// Add repository to local inventory.
+
    AddInventory(RepoId, chan::Sender<bool>),
    /// Connect to node with the given address.
    Connect(NodeId, Address, ConnectOptions),
    /// Disconnect from node.
@@ -247,7 +247,7 @@ impl fmt::Debug for Command {
        match self {
            Self::AnnounceRefs(id, _) => write!(f, "AnnounceRefs({id})"),
            Self::AnnounceInventory => write!(f, "AnnounceInventory"),
-
            Self::UpdateInventory(rid, _) => write!(f, "UpdateInventory({rid})"),
+
            Self::AddInventory(rid, _) => write!(f, "AddInventory({rid})"),
            Self::Connect(id, addr, opts) => write!(f, "Connect({id}, {addr}, {opts:?})"),
            Self::Disconnect(id) => write!(f, "Disconnect({id})"),
            Self::Config(_) => write!(f, "Config"),
@@ -391,6 +391,12 @@ where
    }
}

+
impl<D> AsMut<D> for Stores<D> {
+
    fn as_mut(&mut self) -> &mut D {
+
        &mut self.0
+
    }
+
}
+

impl<D> From<D> for Stores<D> {
    fn from(db: D) -> Self {
        Self(db)
@@ -442,7 +448,7 @@ pub struct Service<D, S, G> {
    /// Last time the service routing table was pruned.
    last_prune: LocalTime,
    /// Last time the inventory was announced.
-
    last_announce: LocalTime,
+
    last_inventory: LocalTime,
    /// Last timestamp used for announcements.
    last_timestamp: Timestamp,
    /// Time when the service was initialized, or `None` if it wasn't initialized.
@@ -520,7 +526,7 @@ where
            last_sync: LocalTime::default(),
            last_prune: LocalTime::default(),
            last_timestamp,
-
            last_announce: LocalTime::default(),
+
            last_inventory: LocalTime::default(),
            started_at: None,     // Updated on initialize.
            last_online_at: None, // Updated on initialize.
            emitter,
@@ -555,16 +561,23 @@ where
    /// simply not announcing it anymore, it will eventually be pruned by nodes.
    pub fn unseed(&mut self, id: &RepoId) -> Result<bool, policy::Error> {
        let updated = self.policies.unseed(id)?;
-
        // Nb. This is potentially slow if we have lots of repos. We should probably
-
        // only re-compute the filter when we've unseeded a certain amount of repos
-
        // and the filter is really out of date.
-
        //
-
        // TODO: Share this code with initialization code.
-
        self.filter = Filter::new(
-
            self.policies
-
                .seed_policies()?
-
                .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)),
-
        );
+

+
        if updated {
+
            // Nb. This is potentially slow if we have lots of repos. We should probably
+
            // only re-compute the filter when we've unseeded a certain amount of repos
+
            // and the filter is really out of date.
+
            //
+
            // TODO: Share this code with initialization code.
+
            self.filter = Filter::new(
+
                self.policies
+
                    .seed_policies()?
+
                    .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)),
+
            );
+
            // Update and announce new inventory.
+
            if let Err(e) = self.remove_inventory(id) {
+
                error!(target: "service", "Error updating inventory after unseed: {e}");
+
            }
+
        }
        Ok(updated)
    }

@@ -623,12 +636,18 @@ where

    /// Lookup a repository, both locally and in the routing table.
    pub fn lookup(&self, rid: RepoId) -> Result<Lookup, LookupError> {
-
        let remote = self.db.routing().get(&rid)?.iter().cloned().collect();
+
        let this = self.nid();
+
        let local = self.storage.get(rid)?;
+
        let remote = self
+
            .db
+
            .routing()
+
            .get(&rid)?
+
            .iter()
+
            .filter(|nid| nid != &this)
+
            .cloned()
+
            .collect();

-
        Ok(Lookup {
-
            local: self.storage.get(rid)?,
-
            remote,
-
        })
+
        Ok(Lookup { local, remote })
    }

    /// Initialize service with current time. Call this once.
@@ -662,27 +681,14 @@ where
            Err(e) => error!(target: "service", "Error checking refs database: {e}"),
        }

-
        // Ensure that our local node is in our address database.
-
        self.db
-
            .addresses_mut()
-
            .insert(
-
                &nid,
-
                self.node.features,
-
                self.node.alias.clone(),
-
                self.node.work(),
-
                self.node.timestamp,
-
                self.node
-
                    .addresses
-
                    .iter()
-
                    .map(|a| KnownAddress::new(a.clone(), address::Source::Peer)),
-
            )
-
            .expect("Service::initialize: error adding local node to address database");
-

        let announced = self
            .db
            .seeds()
            .seeded_by(&nid)?
            .collect::<Result<HashMap<_, _>, _>>()?;
+
        let mut inventory = BTreeSet::new();
+
        let mut private = BTreeSet::new();
+

        for repo in self.storage.repositories()? {
            let rid = repo.rid;

@@ -693,7 +699,9 @@ where
            }
            // Add public repositories to inventory.
            if repo.doc.visibility.is_public() {
-
                self.storage.insert(rid);
+
                inventory.insert(rid);
+
            } else {
+
                private.insert(rid);
            }
            // If we have no owned refs for this repo, then there's nothing to announce.
            let Some(updated_at) = repo.synced_at else {
@@ -723,16 +731,19 @@ where
            }
        }

-
        {
-
            let inventory = self.storage.inventory()?;
-
            // Ensure that our inventory is recorded in our routing table, and we are seeding
-
            // all of it. It can happen that inventory is not properly seeded if for eg. the
-
            // user creates a new repository while the node is stopped.
-
            self.db
-
                .routing_mut()
-
                .insert(inventory.iter(), nid, time.into())?;
-
            self.inventory = gossip::inventory(self.timestamp(), inventory);
-
        }
+
        // Ensure that our inventory is recorded in our routing table, and we are seeding
+
        // all of it. It can happen that inventory is not properly seeded if for eg. the
+
        // user creates a new repository while the node is stopped.
+
        self.db
+
            .routing_mut()
+
            .add_inventory(inventory.iter(), nid, time.into())?;
+
        self.inventory = gossip::inventory(self.timestamp(), inventory);
+

+
        // Ensure that private repositories are not in our inventory. It's possible that
+
        // a repository was public and then it was made private.
+
        self.db
+
            .routing_mut()
+
            .remove_inventories(private.iter(), &nid)?;

        // Setup subscription filter for seeded repos.
        self.filter = Filter::new(
@@ -805,18 +816,16 @@ where
        if now - self.last_sync >= SYNC_INTERVAL {
            trace!(target: "service", "Running 'sync' task...");

-
            if let Err(e) = self.fetch_missing_inventory() {
+
            if let Err(e) = self.fetch_missing_repositories() {
                error!(target: "service", "Error fetching missing inventory: {e}");
            }
            self.outbox.wakeup(SYNC_INTERVAL);
            self.last_sync = now;
        }
-
        if now - self.last_announce >= ANNOUNCE_INTERVAL {
+
        if now - self.last_inventory >= ANNOUNCE_INTERVAL {
            trace!(target: "service", "Running 'announce' task...");

-
            if let Err(err) = self.announce_inventory() {
-
                error!(target: "service", "Error announcing inventory: {err}");
-
            }
+
            self.announce_inventory();
            self.outbox.wakeup(ANNOUNCE_INTERVAL);
        }
        if now - self.last_prune >= PRUNE_INTERVAL {
@@ -939,19 +948,16 @@ where
                }
            }
            Command::AnnounceInventory => {
-
                if let Err(err) = self.announce_inventory() {
-
                    error!(target: "service", "Error announcing inventory: {err}");
-
                }
-
            }
-
            Command::UpdateInventory(rid, resp) => {
-
                self.storage.insert(rid);
-

-
                let synced = self
-
                    .sync_inventory()
-
                    .expect("Service::command: error syncing inventory");
-
                resp.send(synced.added.len() + synced.removed.len() > 0)
-
                    .ok();
+
                self.announce_inventory();
            }
+
            Command::AddInventory(rid, resp) => match self.add_inventory(rid) {
+
                Ok(updated) => {
+
                    resp.send(updated).ok();
+
                }
+
                Err(e) => {
+
                    error!(target: "service", "Error adding {rid} to inventory: {e}");
+
                }
+
            },
            Command::QueryState(query, sender) => {
                sender.send(query(self)).ok();
            }
@@ -1171,8 +1177,9 @@ where
                if clone && doc.visibility.is_public() {
                    debug!(target: "service", "Updating and announcing inventory for cloned repository {rid}..");

-
                    self.storage.insert(rid);
-
                    self.sync_and_announce_inventory();
+
                    if let Err(e) = self.add_inventory(rid) {
+
                        error!(target: "service", "Error announcing inventory for {rid}: {e}");
+
                    }
                }

                // It's possible for a fetch to succeed but nothing was updated.
@@ -1539,8 +1546,6 @@ where
                        return Ok(None);
                    }
                }
-

-
                let inventory = self.storage.inventory_ref();
                let mut missing = Vec::new();

                for id in message.inventory.as_slice() {
@@ -1560,14 +1565,20 @@ where
                        ) {
                            // Only if we do not have the repository locally do we fetch here.
                            // If we do have it, only fetch after receiving a ref announcement.
-
                            if !inventory.contains(id) {
-
                                missing.push(*id);
+
                            match self.db.routing().entry(id, self.nid()) {
+
                                Ok(entry) => {
+
                                    if entry.is_none() {
+
                                        missing.push(*id);
+
                                    }
+
                                }
+
                                Err(e) => error!(
+
                                    target: "service",
+
                                    "Error checking local inventory for {id}: {e}"
+
                                ),
                            }
                        }
                    }
                }
-
                drop(inventory);
-

                for rid in missing {
                    debug!(target: "service", "Missing seeded inventory {rid}; initiating fetch..");
                    self.fetch(rid, *announcer, FETCH_TIMEOUT, None);
@@ -1886,7 +1897,7 @@ where

    /// Add a seed to our routing table.
    fn seed_discovered(&mut self, rid: RepoId, nid: NodeId, time: Timestamp) {
-
        if let Ok(result) = self.db.routing_mut().insert([&rid], nid, time) {
+
        if let Ok(result) = self.db.routing_mut().add_inventory([&rid], nid, time) {
            if let &[(_, InsertResult::SeedAdded)] = result.as_slice() {
                self.emitter.emit(Event::SeedDiscovered { rid, nid });
                info!(target: "service", "Routing table updated for {} with seed {nid}", rid);
@@ -1931,14 +1942,62 @@ where
            > 0
    }

-
    /// Update our routing table with our local node's inventory.
-
    fn sync_inventory(&mut self) -> Result<SyncedRouting, Error> {
-
        let inventory = self.storage.inventory()?;
-
        let result = self.sync_routing(inventory.clone(), self.node_id(), self.clock.into())?;
-
        // Update cached inventory message.
-
        self.inventory = gossip::inventory(self.timestamp(), inventory);
+
    /// Remove a local repository from our inventory.
+
    fn remove_inventory(&mut self, rid: &RepoId) -> Result<bool, Error> {
+
        let node = self.node_id();
+
        let now = self.timestamp();
+

+
        let removed = self.db.routing_mut().remove_inventory(rid, &node)?;
+
        if removed {
+
            self.refresh_and_announce_inventory(now)?;
+
        }
+
        Ok(removed)
+
    }
+

+
    /// Add a local repository to our inventory.
+
    fn add_inventory(&mut self, rid: RepoId) -> Result<bool, Error> {
+
        let node = self.node_id();
+
        let now = self.timestamp();
+

+
        if !self.storage.contains(&rid)? {
+
            error!(target: "service", "Attempt to add non-existing inventory {rid}: repository not found in storage");
+
            return Ok(false);
+
        }
+
        // Add to our local inventory.
+
        let updates = self.db.routing_mut().add_inventory([&rid], node, now)?;
+
        let updated = !updates.is_empty();
+

+
        if updated {
+
            self.refresh_and_announce_inventory(now)?;
+
        }
+
        Ok(updated)
+
    }
+

+
    /// Update cached inventory message, and announce new inventory to peers.
+
    fn refresh_and_announce_inventory(&mut self, time: Timestamp) -> Result<(), Error> {
+
        let inventory = self.inventory()?;

-
        Ok(result)
+
        self.inventory = gossip::inventory(time, inventory);
+
        self.announce_inventory();
+

+
        Ok(())
+
    }
+

+
    /// Get our local inventory.
+
    ///
+
    /// A node's inventory is the advertized list of repositories offered by a node.
+
    ///
+
    /// A node's inventory consists of *public* repositories that are seeded and available locally
+
    /// in the node's storage. We use the routing table as the canonical state of all inventories,
+
    /// including the local node's.
+
    ///
+
    /// When a repository is unseeded, it is also removed from the inventory. Private repositories
+
    /// are *not* part of a node's inventory.
+
    fn inventory(&self) -> Result<HashSet<RepoId>, Error> {
+
        self.db
+
            .routing()
+
            .get_inventory(self.nid())
+
            .map_err(Error::from)
    }

    /// Process a peer inventory announcement by updating our routing table.
@@ -1953,10 +2012,10 @@ where
        let mut synced = SyncedRouting::default();
        let included = inventory.into_iter().collect::<BTreeSet<_>>();

-
        for (rid, result) in self
-
            .db
-
            .routing_mut()
-
            .insert(included.iter(), from, timestamp)?
+
        for (rid, result) in
+
            self.db
+
                .routing_mut()
+
                .add_inventory(included.iter(), from, timestamp)?
        {
            match result {
                InsertResult::SeedAdded => {
@@ -1979,9 +2038,9 @@ where
                InsertResult::NotUpdated => {}
            }
        }
-
        for rid in self.db.routing().get_resources(&from)?.into_iter() {
+
        for rid in self.db.routing().get_inventory(&from)?.into_iter() {
            if !included.contains(&rid) {
-
                if self.db.routing_mut().remove(&rid, &from)? {
+
                if self.db.routing_mut().remove_inventory(&rid, &from)? {
                    synced.removed.push(rid);
                    self.emitter.emit(Event::SeedDropped { rid, nid: from });
                }
@@ -2093,22 +2152,6 @@ where
        Ok((refs, timestamp))
    }

-
    fn sync_and_announce_inventory(&mut self) {
-
        match self.sync_inventory() {
-
            Ok(synced) => {
-
                // Only announce if our inventory changed.
-
                if synced.added.len() + synced.removed.len() > 0 {
-
                    if let Err(e) = self.announce_inventory() {
-
                        error!(target: "service", "Failed to announce inventory: {e}");
-
                    }
-
                }
-
            }
-
            Err(e) => {
-
                error!(target: "service", "Failed to sync inventory: {e}");
-
            }
-
        }
-
    }
-

    fn reconnect(&mut self, nid: NodeId, addr: Address) -> bool {
        if let Some(sess) = self.sessions.get_mut(&nid) {
            sess.to_initial();
@@ -2262,8 +2305,14 @@ where
        Ok(())
    }

-
    /// Announce our inventory to all connected peers.
-
    fn announce_inventory(&mut self) -> Result<(), storage::Error> {
+
    /// Announce our inventory to all connected peers, unless it was already announced.
+
    fn announce_inventory(&mut self) {
+
        let timestamp = self.inventory.timestamp.to_local_time();
+

+
        if self.last_inventory == timestamp {
+
            debug!(target: "service", "Skipping redundant inventory announcement (t={})", self.inventory.timestamp);
+
            return;
+
        }
        let msg = AnnouncementMessage::from(self.inventory.clone());

        self.outbox.announce(
@@ -2271,9 +2320,7 @@ where
            self.sessions.connected().map(|(_, p)| p),
            self.db.gossip_mut(),
        );
-
        self.last_announce = self.clock;
-

-
        Ok(())
+
        self.last_inventory = timestamp;
    }

    fn prune_routing_entries(&mut self, now: &LocalTime) -> Result<(), routing::Error> {
@@ -2355,16 +2402,17 @@ where
        }
    }

-
    /// Fetch all repositories that are seeded but missing from our inventory.
-
    fn fetch_missing_inventory(&mut self) -> Result<(), Error> {
-
        let inventory = self.storage().inventory()?;
-
        let missing = self
-
            .policies
-
            .seed_policies()?
-
            .filter_map(|t| (t.policy.is_allow()).then_some(t.rid))
-
            .filter(|rid| !inventory.contains(rid));
+
    /// Fetch all repositories that are seeded but missing from storage.
+
    fn fetch_missing_repositories(&mut self) -> Result<(), Error> {
+
        for policy in self.policies.seed_policies()? {
+
            let rid = policy.rid;

-
        for rid in missing {
+
            if !policy.is_allow() {
+
                continue;
+
            }
+
            if self.storage.contains(&rid)? {
+
                continue;
+
            }
            match self.seeds(&rid) {
                Ok(seeds) => {
                    if let Some(connected) = NonEmpty::from_vec(seeds.connected().collect()) {
modified radicle-node/src/test/environment.rs
@@ -9,6 +9,7 @@ use std::{

use crossbeam_channel as chan;

+
use localtime::LocalTime;
use radicle::cob::cache::COBS_DB_FILE;
use radicle::cob::issue;
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
@@ -18,7 +19,6 @@ use radicle::git::refname;
use radicle::identity::{RepoId, Visibility};
use radicle::node::config::ConnectAddress;
use radicle::node::policy::store as policy;
-
use radicle::node::routing::Store;
use radicle::node::seed::Store as _;
use radicle::node::Database;
use radicle::node::{Alias, POLICIES_DB_FILE};
@@ -113,21 +113,32 @@ impl Environment {
        let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
        let policies_db = home.node().join(POLICIES_DB_FILE);
        let cobs_db = home.cobs().join(COBS_DB_FILE);
+
        let now = LocalTime::now();

        config.write(&home.config()).unwrap();

        let storage = Storage::open(
            home.storage(),
            git::UserInfo {
-
                alias,
+
                alias: alias.clone(),
                key: keypair.pk.into(),
            },
        )
        .unwrap();
+
        let public_key = keypair.pk.into();

        policy::Store::open(policies_db).unwrap();
-
        home.database_mut().unwrap(); // Just create the database.
        cob::cache::Store::open(cobs_db).unwrap();
+
        home.database_mut()
+
            .unwrap()
+
            .init(
+
                &public_key,
+
                config.node.features(),
+
                Alias::new(alias),
+
                now.into(),
+
                config.node.external_addresses.iter(),
+
            )
+
            .unwrap();

        transport::local::register(storage.clone());
        keystore.store(keypair.clone(), "radicle", None).unwrap();
@@ -139,7 +150,7 @@ impl Environment {
            home,
            storage,
            keystore,
-
            public_key: keypair.pk.into(),
+
            public_key,
            config,
        }
    }
@@ -248,10 +259,15 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {

    /// Get routing table entries.
    pub fn routing(&self) -> impl Iterator<Item = (RepoId, NodeId)> {
-
        Database::reader(self.home.node().join(node::NODE_DB_FILE))
-
            .unwrap()
-
            .entries()
-
            .unwrap()
+
        use node::routing::Store as _;
+

+
        self.home.routing_mut().unwrap().entries().unwrap()
+
    }
+

+
    pub fn inventory(&self) -> impl Iterator<Item = RepoId> + '_ {
+
        self.routing()
+
            .filter(|(_, n)| *n == self.id)
+
            .map(|(r, _)| r)
    }

    /// Get sync status of a repo.
@@ -605,7 +621,7 @@ pub fn converge<'a, G: Signer + cyphernet::Ecdh + 'static>(
            all_routes.insert((rid, seed_id));
        }
        // Routes from the local inventory.
-
        for rid in node.storage.inventory().unwrap() {
+
        for rid in node.inventory() {
            all_routes.insert((rid, node.id));
        }
    }
modified radicle-node/src/test/handle.rs
@@ -104,7 +104,7 @@ impl radicle::node::Handle for Handle {
        Ok(())
    }

-
    fn update_inventory(&mut self, _rid: RepoId) -> Result<bool, Self::Error> {
+
    fn add_inventory(&mut self, _rid: RepoId) -> Result<bool, Self::Error> {
        unimplemented!()
    }

modified radicle-node/src/test/peer.rs
@@ -1,4 +1,5 @@
#![allow(dead_code)]
+
use std::collections::HashSet;
use std::iter;
use std::net;
use std::ops::{Deref, DerefMut};
@@ -19,6 +20,7 @@ use crate::crypto::test::signer::MockSigner;
use crate::crypto::Signer;
use crate::identity::RepoId;
use crate::node;
+
use crate::node::routing::Store as _;
use crate::prelude::*;
use crate::runtime::Emitter;
use crate::service;
@@ -27,7 +29,6 @@ use crate::service::message::*;
use crate::service::policy::{Scope, SeedingPolicy};
use crate::service::*;
use crate::storage::git::transport::remote;
-
use crate::storage::Inventory;
use crate::storage::{RemoteId, WriteStorage};
use crate::test::storage::MockStorage;
use crate::test::{arbitrary, fixtures, simulator};
@@ -99,7 +100,6 @@ where

pub struct Config<G: Signer + 'static> {
    pub config: service::Config,
-
    pub db: Stores<node::Database>,
    pub local_time: LocalTime,
    pub policy: SeedingPolicy,
    pub signer: G,
@@ -112,13 +112,10 @@ impl Default for Config<MockSigner> {
        let mut rng = fastrand::Rng::new();
        let signer = MockSigner::new(&mut rng);
        let tmp = tempfile::TempDir::new().unwrap();
-
        let db = Database::open(tmp.path().join(node::NODE_DB_FILE))
-
            .unwrap()
-
            .into();
+
        let config = service::Config::test(Alias::from_str("mocky").unwrap());

        Config {
-
            config: service::Config::test(Alias::from_str("mocky").unwrap()),
-
            db,
+
            config,
            local_time: LocalTime::now(),
            policy: SeedingPolicy::default(),
            signer,
@@ -164,19 +161,32 @@ where
        let id = *config.signer.public_key();
        let ip = ip.into();
        let local_addr = net::SocketAddr::new(ip, config.rng.u16(..));
-
        let inventory = storage.inventory().unwrap();
+
        let inventory = storage.repositories().unwrap();

        // Make sure the peer address is advertized.
        config.config.external_addresses.push(local_addr.into());
-
        for rid in &inventory {
-
            policies.seed(rid, Scope::Followed).unwrap();
+
        for repo in &inventory {
+
            policies.seed(&repo.rid, Scope::Followed).unwrap();
        }
+
        // Initialize database.
+
        let db = Database::open(config.tmp.path().join(node::NODE_DB_FILE))
+
            .unwrap()
+
            .init(
+
                &id,
+
                config.config.features(),
+
                config.config.alias.clone(),
+
                config.local_time.into(),
+
                config.config.external_addresses.iter(),
+
            )
+
            .unwrap()
+
            .into();
+

        let announcement =
            service::gossip::node(&config.config, Timestamp::from(config.local_time) + 1);
        let emitter: Emitter<Event> = Default::default();
        let service = Service::new(
            config.config,
-
            config.db,
+
            db,
            storage,
            policies,
            config.signer,
@@ -255,8 +265,12 @@ where
        (*self.clock()).into()
    }

-
    pub fn inventory(&self) -> Inventory {
-
        self.service.storage().inventory().unwrap()
+
    pub fn inventory(&self) -> HashSet<RepoId> {
+
        self.service
+
            .database()
+
            .routing()
+
            .get_inventory(self.nid())
+
            .unwrap()
    }

    pub fn git_url(&self, repo: RepoId, namespace: Option<RemoteId>) -> remote::Url {
modified radicle-node/src/tests.rs
@@ -274,21 +274,21 @@ fn test_inventory_sync() {
    let bob_storage = fixtures::storage(tmp.path().join("bob"), &bob_signer).unwrap();
    let bob = Peer::with_storage("bob", [8, 8, 8, 8], bob_storage);
    let now = LocalTime::now().into();
-
    let projs = bob.storage().inventory().unwrap();
+
    let repos = bob.inventory().into_iter().collect::<Vec<_>>();

    alice.connect_to(&bob);
    alice.receive(
        bob.id(),
        Message::inventory(
            InventoryAnnouncement {
-
                inventory: projs.clone().try_into().unwrap(),
+
                inventory: repos.clone().try_into().unwrap(),
                timestamp: now,
            },
            bob.signer(),
        ),
    );

-
    for proj in &projs {
+
    for proj in &repos {
        let seeds = alice.database().routing().get(proj).unwrap();
        assert!(seeds.contains(&bob.node_id()));
    }
@@ -701,12 +701,7 @@ fn test_refs_announcement_relay() {
        )
        .initialized()
    };
-
    let bob_inv = bob
-
        .storage()
-
        .inventory()
-
        .unwrap()
-
        .into_iter()
-
        .collect::<Vec<_>>();
+
    let bob_inv = bob.inventory().into_iter().collect::<Vec<_>>();

    alice.seed(&bob_inv[0], policy::Scope::All).unwrap();
    alice.seed(&bob_inv[1], policy::Scope::All).unwrap();
@@ -779,8 +774,8 @@ fn test_refs_announcement_fetch_trusted_no_inventory() {
        )
        .initialized()
    };
-
    let bob_inv = bob.storage().inventory().unwrap();
-
    let rid = bob_inv.first().unwrap();
+
    let bob_inv = bob.inventory();
+
    let rid = bob_inv.iter().next().unwrap();

    alice.seed(rid, policy::Scope::Followed).unwrap();
    alice.connect_to(&bob);
@@ -887,10 +882,7 @@ fn test_refs_announcement_offline() {
            },
        )
    };
-
    let mut inv = alice.inventory();
-
    let rid = *inv.first().unwrap();
    let mut bob = Peer::new("bob", [8, 8, 8, 8]);
-
    bob.seed(&rid, policy::Scope::All).unwrap();

    // Make sure alice's service wasn't initialized before.
    assert_eq!(*alice.clock(), LocalTime::default());
@@ -899,6 +891,11 @@ fn test_refs_announcement_offline() {
    alice.connect_to(&bob);
    alice.receive(bob.id, Message::Subscribe(Subscribe::all()));

+
    let mut inv = alice.inventory();
+
    let rid = *inv.iter().next().unwrap();
+

+
    bob.seed(&rid, policy::Scope::All).unwrap();
+

    // Alice announces the refs of all projects since she hasn't announced refs for these projects
    // yet.
    for msg in alice.messages(bob.id()) {
@@ -1429,7 +1426,7 @@ fn test_queued_fetch_max_capacity() {

#[test]
fn test_queued_fetch_from_ann_same_rid() {
-
    let storage = arbitrary::nonempty_storage(3);
+
    let storage = arbitrary::nonempty_storage(1); // We're testing both public and private repos.
    let mut repo_keys = storage.repos.keys();
    let rid = *repo_keys.next().unwrap();
    let mut alice = Peer::with_storage("alice", [7, 7, 7, 7], storage);
@@ -1448,8 +1445,6 @@ fn test_queued_fetch_from_ann_same_rid() {
        timestamp: bob.timestamp(),
    };

-
    logger::init(log::Level::Trace);
-

    alice.seed(&rid, policy::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.connect_to(&eve);
@@ -1685,7 +1680,7 @@ fn test_init_and_seed() {
    // We now expect Eve to fetch Alice's project from Alice.
    // Then we expect Bob to fetch Alice's project from Eve.
    alice.elapse(LocalDuration::from_secs(1)); // Make sure our announcement is fresh.
-
    alice.command(service::Command::UpdateInventory(proj_id, send));
+
    alice.command(service::Command::AddInventory(proj_id, send));

    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());

@@ -1713,9 +1708,27 @@ fn test_init_and_seed() {
fn prop_inventory_exchange_dense() {
    fn property(alice_inv: MockStorage, bob_inv: MockStorage, eve_inv: MockStorage) {
        let rng = fastrand::Rng::new();
-
        let alice = Peer::with_storage("alice", [7, 7, 7, 7], alice_inv.clone());
-
        let mut bob = Peer::with_storage("bob", [8, 8, 8, 8], bob_inv.clone());
-
        let mut eve = Peer::with_storage("eve", [9, 9, 9, 9], eve_inv.clone());
+
        let alice = Peer::with_storage(
+
            "alice",
+
            [7, 7, 7, 7],
+
            alice_inv
+
                .clone()
+
                .map(|doc| doc.visibility = Visibility::Public),
+
        );
+
        let mut bob = Peer::with_storage(
+
            "bob",
+
            [8, 8, 8, 8],
+
            bob_inv
+
                .clone()
+
                .map(|doc| doc.visibility = Visibility::Public),
+
        );
+
        let mut eve = Peer::with_storage(
+
            "eve",
+
            [9, 9, 9, 9],
+
            eve_inv
+
                .clone()
+
                .map(|doc| doc.visibility = Visibility::Public),
+
        );
        let mut routing = RandomMap::with_hasher(rng.clone().into());

        for (inv, peer) in &[
@@ -1912,8 +1925,7 @@ fn test_announcement_message_amplification() {
            .storage_mut()
            .repos
            .insert(rid, gen::<MockRepository>(1));
-
        alice.command(Command::UpdateInventory(rid, tx));
-
        alice.command(Command::AnnounceInventory);
+
        alice.command(Command::AddInventory(rid, tx));

        sim.run_while([&mut alice, &mut bob, &mut eve, &mut zod, &mut tom], |s| {
            s.elapsed() < LocalDuration::from_mins(3)
modified radicle-term/src/table.rs
@@ -92,6 +92,11 @@ where
        let inner = self.inner(parent);
        let cols = inner.cols;

+
        // Don't print empty tables.
+
        if self.is_empty() {
+
            return lines;
+
        }
+

        if let Some(color) = border {
            lines.push(
                Line::default()
modified radicle/src/node.rs
@@ -463,7 +463,7 @@ pub enum Command {
    AnnounceInventory,

    /// Update node's inventory.
-
    UpdateInventory { rid: RepoId },
+
    AddInventory { rid: RepoId },

    /// Get the current node condiguration.
    Config,
@@ -924,7 +924,7 @@ pub trait Handle: Clone + Sync + Send {
    /// Announce local inventory.
    fn announce_inventory(&mut self) -> Result<(), Self::Error>;
    /// Notify the service that our inventory was updated with the given repository.
-
    fn update_inventory(&mut self, rid: RepoId) -> Result<bool, Self::Error>;
+
    fn add_inventory(&mut self, rid: RepoId) -> Result<bool, Self::Error>;
    /// Ask the service to shutdown.
    fn shutdown(self) -> Result<(), Self::Error>;
    /// Query the peer session state.
@@ -1224,8 +1224,8 @@ impl Handle for Node {
        Ok(())
    }

-
    fn update_inventory(&mut self, rid: RepoId) -> Result<bool, Error> {
-
        let mut lines = self.call::<Success>(Command::UpdateInventory { rid }, DEFAULT_TIMEOUT)?;
+
    fn add_inventory(&mut self, rid: RepoId) -> Result<bool, Error> {
+
        let mut lines = self.call::<Success>(Command::AddInventory { rid }, DEFAULT_TIMEOUT)?;
        let response = lines.next().ok_or(Error::EmptyResponse)??;

        Ok(response.updated)
modified radicle/src/node/db.rs
@@ -15,6 +15,7 @@ use std::{fmt, time};
use sqlite as sql;
use thiserror::Error;

+
use crate::node::{address, Address, Alias, Features, KnownAddress, NodeId, Timestamp};
use crate::sql::transaction;

/// How long to wait for the database lock to be released before failing a read.
@@ -34,6 +35,9 @@ const MIGRATIONS: &[&str] = &[

#[derive(Error, Debug)]
pub enum Error {
+
    /// Initialization error.
+
    #[error("error initializing the database: {0}")]
+
    Init(#[from] address::store::Error),
    /// An Internal error.
    #[error("internal error: {0}")]
    Internal(#[from] sql::Error),
@@ -121,6 +125,30 @@ impl Database {
        Ok(self)
    }

+
    /// Initialize by adding our local node to the database.
+
    pub fn init<'a>(
+
        mut self,
+
        node: &NodeId,
+
        features: Features,
+
        alias: Alias,
+
        timestamp: Timestamp,
+
        addrs: impl IntoIterator<Item = &'a Address>,
+
    ) -> Result<Self, Error> {
+
        address::Store::insert(
+
            &mut self,
+
            node,
+
            features,
+
            alias,
+
            0,
+
            timestamp,
+
            addrs
+
                .into_iter()
+
                .map(|a| KnownAddress::new(a.clone(), address::Source::Imported)),
+
        )?;
+

+
        Ok(self)
+
    }
+

    /// Create a new in-memory database.
    pub fn memory() -> Result<Self, Error> {
        let db = sql::Connection::open_thread_safe(":memory:")?;
modified radicle/src/node/routing.rs
@@ -36,8 +36,8 @@ pub enum Error {
pub trait Store {
    /// Get the nodes seeding the given id.
    fn get(&self, id: &RepoId) -> Result<HashSet<NodeId>, Error>;
-
    /// Get the resources seeded by the given node.
-
    fn get_resources(&self, node_id: &NodeId) -> Result<HashSet<RepoId>, Error>;
+
    /// Get the inventory seeded by the given node.
+
    fn get_inventory(&self, node_id: &NodeId) -> Result<HashSet<RepoId>, Error>;
    /// Get a specific entry.
    fn entry(&self, id: &RepoId, node: &NodeId) -> Result<Option<Timestamp>, Error>;
    /// Checks if any entries are available.
@@ -45,14 +45,20 @@ pub trait Store {
        Ok(self.len()? == 0)
    }
    /// Add a new node seeding the given id.
-
    fn insert<'a>(
+
    fn add_inventory<'a>(
        &mut self,
        ids: impl IntoIterator<Item = &'a RepoId>,
        node: NodeId,
        time: Timestamp,
    ) -> Result<Vec<(RepoId, InsertResult)>, Error>;
-
    /// Remove a node for the given id.
-
    fn remove(&mut self, id: &RepoId, node: &NodeId) -> Result<bool, Error>;
+
    /// Remove an inventory from the given node.
+
    fn remove_inventory(&mut self, id: &RepoId, node: &NodeId) -> Result<bool, Error>;
+
    /// Remove multiple inventories from the given node.
+
    fn remove_inventories<'a>(
+
        &mut self,
+
        ids: impl IntoIterator<Item = &'a RepoId>,
+
        node: &NodeId,
+
    ) -> Result<(), Error>;
    /// Iterate over all entries in the routing table.
    fn entries(&self) -> Result<Box<dyn Iterator<Item = (RepoId, NodeId)>>, Error>;
    /// Get the total number of routing entries.
@@ -77,15 +83,15 @@ impl Store for Database {
        Ok(nodes)
    }

-
    fn get_resources(&self, node: &NodeId) -> Result<HashSet<RepoId>, Error> {
+
    fn get_inventory(&self, node: &NodeId) -> Result<HashSet<RepoId>, Error> {
        let mut stmt = self.db.prepare("SELECT repo FROM routing WHERE node = ?")?;
        stmt.bind((1, node))?;

-
        let mut resources = HashSet::new();
+
        let mut inventory = HashSet::new();
        for row in stmt.into_iter() {
-
            resources.insert(row?.read::<RepoId, _>("repo"));
+
            inventory.insert(row?.read::<RepoId, _>("repo"));
        }
-
        Ok(resources)
+
        Ok(inventory)
    }

    fn entry(&self, id: &RepoId, node: &NodeId) -> Result<Option<Timestamp>, Error> {
@@ -102,7 +108,7 @@ impl Store for Database {
        Ok(None)
    }

-
    fn insert<'a>(
+
    fn add_inventory<'a>(
        &mut self,
        ids: impl IntoIterator<Item = &'a RepoId>,
        node: NodeId,
@@ -160,7 +166,7 @@ impl Store for Database {
        Ok(Box::new(entries.into_iter()))
    }

-
    fn remove(&mut self, id: &RepoId, node: &NodeId) -> Result<bool, Error> {
+
    fn remove_inventory(&mut self, id: &RepoId, node: &NodeId) -> Result<bool, Error> {
        let mut stmt = self
            .db
            .prepare("DELETE FROM routing WHERE repo = ? AND node = ?")?;
@@ -172,6 +178,27 @@ impl Store for Database {
        Ok(self.db.change_count() > 0)
    }

+
    fn remove_inventories<'a>(
+
        &mut self,
+
        rids: impl IntoIterator<Item = &'a RepoId>,
+
        nid: &NodeId,
+
    ) -> Result<(), Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("DELETE FROM routing WHERE repo = ? AND node = ?")?;
+

+
        transaction(&self.db, |_| {
+
            for rid in rids.into_iter() {
+
                stmt.bind((1, rid))?;
+
                stmt.bind((2, nid))?;
+

+
                stmt.iter().next();
+
                stmt.reset()?;
+
            }
+
            Ok::<_, Error>(())
+
        })
+
    }
+

    fn len(&self) -> Result<usize, Error> {
        let stmt = self.db.prepare("SELECT COUNT(1) FROM routing")?;
        let count: i64 = stmt
@@ -242,7 +269,7 @@ mod test {

        for node in &nodes {
            assert_eq!(
-
                db.insert(&ids, *node, Timestamp::EPOCH).unwrap(),
+
                db.add_inventory(&ids, *node, Timestamp::EPOCH).unwrap(),
                ids.iter()
                    .map(|id| (*id, InsertResult::SeedAdded))
                    .collect::<Vec<_>>()
@@ -264,11 +291,11 @@ mod test {
        let mut db = database(":memory:");

        for node in &nodes {
-
            db.insert(&ids, *node, Timestamp::EPOCH).unwrap();
+
            db.add_inventory(&ids, *node, Timestamp::EPOCH).unwrap();
        }

        for node in &nodes {
-
            let projects = db.get_resources(node).unwrap();
+
            let projects = db.get_inventory(node).unwrap();
            for id in &ids {
                assert!(projects.contains(id));
            }
@@ -283,7 +310,7 @@ mod test {

        for node in &nodes {
            assert!(db
-
                .insert(&ids, *node, Timestamp::EPOCH)
+
                .add_inventory(&ids, *node, Timestamp::EPOCH)
                .unwrap()
                .iter()
                .all(|(_, r)| *r == InsertResult::SeedAdded));
@@ -305,11 +332,11 @@ mod test {
        let mut db = database(":memory:");

        for node in &nodes {
-
            db.insert(&ids, *node, Timestamp::EPOCH).unwrap();
+
            db.add_inventory(&ids, *node, Timestamp::EPOCH).unwrap();
        }
        for id in &ids {
            for node in &nodes {
-
                assert!(db.remove(id, node).unwrap());
+
                assert!(db.remove_inventory(id, node).unwrap());
            }
        }
        for id in &ids {
@@ -324,15 +351,15 @@ mod test {
        let mut db = database(":memory:");

        assert_eq!(
-
            db.insert([&id], node, Timestamp::EPOCH).unwrap(),
+
            db.add_inventory([&id], node, Timestamp::EPOCH).unwrap(),
            vec![(id, InsertResult::SeedAdded)]
        );
        assert_eq!(
-
            db.insert([&id], node, Timestamp::EPOCH).unwrap(),
+
            db.add_inventory([&id], node, Timestamp::EPOCH).unwrap(),
            vec![(id, InsertResult::NotUpdated)]
        );
        assert_eq!(
-
            db.insert([&id], node, Timestamp::EPOCH).unwrap(),
+
            db.add_inventory([&id], node, Timestamp::EPOCH).unwrap(),
            vec![(id, InsertResult::NotUpdated)]
        );
    }
@@ -344,11 +371,11 @@ mod test {
        let mut db = database(":memory:");

        assert_eq!(
-
            db.insert([&id], node, Timestamp::EPOCH).unwrap(),
+
            db.add_inventory([&id], node, Timestamp::EPOCH).unwrap(),
            vec![(id, InsertResult::SeedAdded)]
        );
        assert_eq!(
-
            db.insert([&id], node, Timestamp::try_from(1u64).unwrap())
+
            db.add_inventory([&id], node, Timestamp::try_from(1u64).unwrap())
                .unwrap(),
            vec![(id, InsertResult::TimeUpdated)]
        );
@@ -366,18 +393,19 @@ mod test {
        let mut db = database(":memory:");

        assert_eq!(
-
            db.insert([&id1], node, Timestamp::EPOCH).unwrap(),
+
            db.add_inventory([&id1], node, Timestamp::EPOCH).unwrap(),
            vec![(id1, InsertResult::SeedAdded)]
        );
        assert_eq!(
-
            db.insert([&id1, &id2], node, Timestamp::EPOCH).unwrap(),
+
            db.add_inventory([&id1, &id2], node, Timestamp::EPOCH)
+
                .unwrap(),
            vec![
                (id1, InsertResult::NotUpdated),
                (id2, InsertResult::SeedAdded)
            ]
        );
        assert_eq!(
-
            db.insert([&id1, &id2], node, Timestamp::try_from(1u64).unwrap())
+
            db.add_inventory([&id1, &id2], node, Timestamp::try_from(1u64).unwrap())
                .unwrap(),
            vec![
                (id1, InsertResult::TimeUpdated),
@@ -393,11 +421,27 @@ mod test {
        let mut db = database(":memory:");

        assert_eq!(
-
            db.insert([&id], node, Timestamp::EPOCH).unwrap(),
+
            db.add_inventory([&id], node, Timestamp::EPOCH).unwrap(),
            vec![(id, InsertResult::SeedAdded)]
        );
-
        assert!(db.remove(&id, &node).unwrap());
-
        assert!(!db.remove(&id, &node).unwrap());
+
        assert!(db.remove_inventory(&id, &node).unwrap());
+
        assert!(!db.remove_inventory(&id, &node).unwrap());
+
    }
+

+
    #[test]
+
    fn test_remove_many() {
+
        let id1 = arbitrary::gen::<RepoId>(1);
+
        let id2 = arbitrary::gen::<RepoId>(1);
+
        let id3 = arbitrary::gen::<RepoId>(1);
+
        let node = arbitrary::gen::<NodeId>(1);
+
        let mut db = database(":memory:");
+

+
        db.add_inventory([&id1, &id2, &id3], node, Timestamp::EPOCH)
+
            .unwrap();
+
        assert_eq!(db.len().unwrap(), 3);
+

+
        db.remove_inventories([&id1, &id3], &node).unwrap();
+
        assert_eq!(db.len().unwrap(), 1);
    }

    #[test]
@@ -406,7 +450,8 @@ mod test {
        let ids = arbitrary::vec::<RepoId>(10);
        let node = arbitrary::gen(1);

-
        db.insert(&ids, node, LocalTime::now().into()).unwrap();
+
        db.add_inventory(&ids, node, LocalTime::now().into())
+
            .unwrap();

        assert_eq!(10, db.len().unwrap(), "correct number of rows in table");
    }
@@ -421,7 +466,7 @@ mod test {

        for node in &nodes {
            let time = rng.u64(..now.as_millis());
-
            db.insert(&ids, *node, Timestamp::try_from(time).unwrap())
+
            db.add_inventory(&ids, *node, Timestamp::try_from(time).unwrap())
                .unwrap();
        }

@@ -430,7 +475,7 @@ mod test {

        for node in &nodes {
            let time = rng.u64(now.as_millis()..i64::MAX as u64);
-
            db.insert(&ids, *node, Timestamp::try_from(time).unwrap())
+
            db.add_inventory(&ids, *node, Timestamp::try_from(time).unwrap())
                .unwrap();
        }

@@ -452,7 +497,7 @@ mod test {
        let mut db = database(":memory:");

        for node in &nodes {
-
            db.insert([&id], *node, Timestamp::EPOCH).unwrap();
+
            db.add_inventory([&id], *node, Timestamp::EPOCH).unwrap();
        }
        assert_eq!(db.count(&id).unwrap(), nodes.len());
    }
modified radicle/src/profile.rs
@@ -14,6 +14,7 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fs, io};

+
use localtime::LocalTime;
use serde::Serialize;
use serde_json as json;
use thiserror::Error;
@@ -26,9 +27,9 @@ use crate::node::policy::config::store::Read;
use crate::node::{
    notifications, policy,
    policy::{Policy, Scope, SeedingPolicy},
-
    Alias, AliasStore,
+
    Alias, AliasStore, Handle as _, Node,
};
-
use crate::prelude::{Did, NodeId};
+
use crate::prelude::{Did, NodeId, RepoId};
use crate::storage::git::transport;
use crate::storage::git::Storage;
use crate::storage::ReadRepository;
@@ -157,6 +158,10 @@ pub enum Error {
    #[error(transparent)]
    Config(#[from] ConfigError),
    #[error(transparent)]
+
    Node(#[from] node::Error),
+
    #[error(transparent)]
+
    Routing(#[from] node::routing::Error),
+
    #[error(transparent)]
    Keystore(#[from] keystore::Error),
    #[error(transparent)]
    MemorySigner(#[from] keystore::MemorySignerError),
@@ -307,8 +312,16 @@ impl Profile {
        )?;
        // Create DBs.
        home.policies_mut()?;
-
        home.database_mut()?;
        home.notifications_mut()?;
+
        home.database_mut()?
+
            .journal_mode(node::db::JournalMode::default())?
+
            .init(
+
                &public_key,
+
                config.node.features(),
+
                config.node.alias.clone(),
+
                LocalTime::now().into(),
+
                config.node.external_addresses.iter(),
+
            )?;

        transport::local::register(storage.clone());

@@ -414,6 +427,57 @@ impl Profile {

        Aliases { policies, db }
    }
+

+
    /// Add the repo to our inventory.
+
    /// If the node is offline, adds it directly to the database.
+
    pub fn add_inventory(&self, rid: RepoId, node: &mut Node) -> Result<bool, Error> {
+
        match node.add_inventory(rid) {
+
            Ok(updated) => Ok(updated),
+
            Err(e) if e.is_connection_err() => {
+
                let now = LocalTime::now();
+
                let mut db = self.database_mut()?;
+
                let updates =
+
                    node::routing::Store::add_inventory(&mut db, [&rid], *self.id(), now.into())?;
+

+
                Ok(!updates.is_empty())
+
            }
+
            Err(e) => Err(e.into()),
+
        }
+
    }
+

+
    /// Seed a repository by first trying to seed through the node, and if the node isn't running,
+
    /// by updating the policy database directly. If the repo is available locally, we also add it
+
    /// to our inventory.
+
    pub fn seed(&self, rid: RepoId, scope: Scope, node: &mut Node) -> Result<bool, Error> {
+
        match node.seed(rid, scope) {
+
            Ok(updated) => Ok(updated),
+
            Err(e) if e.is_connection_err() => {
+
                let mut config = self.policies_mut()?;
+
                let updated = config.seed(&rid, scope)?;
+

+
                Ok(updated)
+
            }
+
            Err(e) => Err(e.into()),
+
        }
+
    }
+

+
    /// Unseed a repository by first trying to unseed through the node, and if the node isn't
+
    /// running, by updating the policy database directly.
+
    pub fn unseed(&self, rid: RepoId, node: &mut Node) -> Result<bool, Error> {
+
        match node.unseed(rid) {
+
            Ok(updated) => Ok(updated),
+
            Err(e) if e.is_connection_err() => {
+
                let mut config = self.policies_mut()?;
+
                let result = config.unseed(&rid)?;
+

+
                let mut db = self.database_mut()?;
+
                node::routing::Store::remove_inventory(&mut db, &rid, self.id())?;
+

+
                Ok(result)
+
            }
+
            Err(e) => Err(e.into()),
+
        }
+
    }
}

impl std::ops::Deref for Profile {
@@ -576,6 +640,21 @@ impl Home {
        Ok(db)
    }

+
    /// Returns the address store.
+
    pub fn addresses(&self) -> Result<impl node::address::Store, node::db::Error> {
+
        self.database_mut()
+
    }
+

+
    /// Returns the routing store.
+
    pub fn routing(&self) -> Result<impl node::routing::Store, node::db::Error> {
+
        self.database()
+
    }
+

+
    /// Returns the routing store, mutably.
+
    pub fn routing_mut(&self) -> Result<impl node::routing::Store, node::db::Error> {
+
        self.database_mut()
+
    }
+

    /// Return a read-only handle for the issues cache.
    pub fn issues<'a, R>(
        &self,
modified radicle/src/rad.rs
@@ -73,9 +73,6 @@ pub fn init<G: Signer, S: WriteStorage>(
    let (project, _) = Repository::init(&doc, &storage, signer)?;
    let url = git::Url::from(project.id);

-
    // Update inventory cache for this storage instance.
-
    storage.insert(project.id);
-

    match init_configure(repo, &project, pk, &default_branch, &url, signer) {
        Ok(signed) => Ok((project.id, doc, signed)),
        Err(err) => {
modified radicle/src/storage.rs
@@ -1,7 +1,7 @@
pub mod git;
pub mod refs;

-
use std::collections::{hash_map, BTreeSet, HashSet};
+
use std::collections::{hash_map, HashSet};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{fmt, io};
@@ -29,7 +29,6 @@ use self::git::UserInfo;
use self::refs::{RefsAt, SignedRefs};

pub type BranchName = git::RefString;
-
pub type Inventory = BTreeSet<RepoId>;

/// Basic repository information.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -150,8 +149,6 @@ pub enum Error {
    Ext(#[from] git::ext::Error),
    #[error("invalid repository identifier {0:?}")]
    InvalidId(std::ffi::OsString),
-
    #[error("inventory: {0}")]
-
    Inventory(io::Error),
    #[error("i/o: {0}")]
    Io(#[from] io::Error),
}
@@ -405,9 +402,6 @@ impl<V> Deref for Remote<V> {
/// Read-only operations on a storage instance.
pub trait ReadStorage {
    type Repository: ReadRepository;
-
    type InventoryRef<'a>: Deref<Target = Inventory>
-
    where
-
        Self: 'a;

    /// Get user info for this storage.
    fn info(&self) -> &UserInfo;
@@ -417,16 +411,8 @@ pub trait ReadStorage {
    fn path_of(&self, rid: &RepoId) -> PathBuf;
    /// Check whether storage contains a repository.
    fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError>;
-
    /// Get the inventory of repositories hosted under this storage.
-
    /// This function should typically only return public repositories.
-
    fn inventory(&self) -> Result<Inventory, Error>;
-
    /// Return a reference to the inventory. Note that this function may hold a
-
    /// lock to the inventory.
-
    fn inventory_ref<'a, 's: 'a>(&'s self) -> Self::InventoryRef<'a>;
    /// Return all repositories (public and private).
    fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error>;
-
    /// Insert this repository into the inventory.
-
    fn insert(&self, rid: RepoId);
    /// Open or create a read-only repository.
    fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError>;
    /// Get a repository's identity if it exists.
@@ -667,7 +653,6 @@ where
    S: ReadStorage + 'static,
{
    type Repository = S::Repository;
-
    type InventoryRef<'a> = S::InventoryRef<'a> where T: 'a;

    fn info(&self) -> &UserInfo {
        self.deref().info()
@@ -681,25 +666,10 @@ where
        self.deref().path_of(rid)
    }

-
    fn insert(&self, rid: RepoId) {
-
        self.deref().insert(rid)
-
    }
-

    fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError> {
        self.deref().contains(rid)
    }

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        self.deref().inventory()
-
    }
-

-
    fn inventory_ref<'a, 's: 'a>(&'s self) -> Self::InventoryRef<'a>
-
    where
-
        T: 'a,
-
    {
-
        self.deref().inventory_ref()
-
    }
-

    fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError> {
        self.deref().get(rid)
    }
modified radicle/src/storage/git.rs
@@ -5,7 +5,6 @@ pub mod transport;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
-
use std::sync::{Arc, Mutex, MutexGuard};
use std::{fs, io};

use crypto::{Signer, Verified};
@@ -20,8 +19,8 @@ use crate::node::SyncedAt;
use crate::storage::refs;
use crate::storage::refs::{Refs, SignedRefs, SignedRefsAt};
use crate::storage::{
-
    Inventory, ReadRepository, ReadStorage, Remote, Remotes, RepositoryError, RepositoryInfo,
-
    SetHead, SignRepository, WriteRepository, WriteStorage,
+
    ReadRepository, ReadStorage, Remote, Remotes, RepositoryError, RepositoryInfo, SetHead,
+
    SignRepository, WriteRepository, WriteStorage,
};
use crate::{git, node};

@@ -73,34 +72,14 @@ impl<'a> TryFrom<git2::Reference<'a>> for Ref {
    }
}

-
/// A reference to a locked inventory.
-
pub struct InventoryLock<'a> {
-
    guard: MutexGuard<'a, Option<BTreeSet<RepoId>>>,
-
}
-

-
impl<'a> Deref for InventoryLock<'a> {
-
    type Target = BTreeSet<RepoId>;
-

-
    #[track_caller]
-
    fn deref(&self) -> &Self::Target {
-
        match *self.guard {
-
            Some(ref cache) => cache,
-
            None => panic!("InventoryLock::deref: inventory cache was not initialized"),
-
        }
-
    }
-
}
-

#[derive(Debug, Clone)]
pub struct Storage {
    path: PathBuf,
    info: UserInfo,
-
    /// Inventory cache. Set to `None` until the cache is populated.
-
    inventory: Arc<Mutex<Option<BTreeSet<RepoId>>>>,
}

impl ReadStorage for Storage {
    type Repository = Repository;
-
    type InventoryRef<'a> = InventoryLock<'a>;

    fn info(&self) -> &UserInfo {
        &self.info
@@ -122,44 +101,6 @@ impl ReadStorage for Storage {
        Ok(false)
    }

-
    fn inventory_ref<'a, 's: 'a>(&'s self) -> InventoryLock<'a> {
-
        let guard = self
-
            .inventory
-
            .lock()
-
            .unwrap_or_else(|poisoned| poisoned.into_inner());
-

-
        InventoryLock { guard }
-
    }
-

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        let mut cache = self
-
            .inventory
-
            .lock()
-
            .unwrap_or_else(|poisoned| poisoned.into_inner());
-

-
        match *cache {
-
            Some(ref cache) => Ok(cache.clone()),
-
            None => {
-
                let repos: BTreeSet<_> = self.public_repositories()?.collect();
-
                *cache = Some(repos.clone());
-
                Ok(repos)
-
            }
-
        }
-
    }
-

-
    fn insert(&self, rid: RepoId) {
-
        let mut repos = self
-
            .inventory
-
            .lock()
-
            .unwrap_or_else(|poisoned| poisoned.into_inner());
-

-
        if let Some(ref mut repos) = *repos {
-
            repos.insert(rid);
-
        } else {
-
            *repos = Some(BTreeSet::from_iter([rid]));
-
        }
-
    }
-

    fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError> {
        Repository::open(paths::repository(self, &rid), rid)
    }
@@ -265,11 +206,7 @@ impl Storage {
            Err(err) => return Err(Error::Io(err)),
            Ok(()) => {}
        }
-
        Ok(Self {
-
            path,
-
            info,
-
            inventory: Default::default(),
-
        })
+
        Ok(Self { path, info })
    }

    /// Create a [`Repository`] in a temporary directory.
@@ -336,14 +273,6 @@ impl Storage {
        }
        Ok(())
    }
-

-
    fn public_repositories(&self) -> Result<impl Iterator<Item = RepoId>, Error> {
-
        let repos = self.repositories()?;
-
        Ok(repos
-
            .into_iter()
-
            .filter(|r| r.doc.visibility.is_public())
-
            .map(|r| r.rid))
-
    }
}

/// Git implementation of [`WriteRepository`] using the `git2` crate.
modified radicle/src/test/storage.rs
@@ -1,4 +1,4 @@
-
use std::collections::{BTreeSet, HashMap};
+
use std::collections::HashMap;
use std::convert::Infallible;
use std::io;
use std::path::{Path, PathBuf};
@@ -51,26 +51,20 @@ impl MockStorage {
            .expect("MockStorage::repo_mut: repository does not exist")
    }

-
    pub fn empty() -> Self {
-
        Self::new(Vec::new())
+
    pub fn map(mut self, f: impl Fn(&mut Doc<Verified>)) -> Self {
+
        for repo in self.repos.values_mut() {
+
            f(&mut repo.doc.doc);
+
        }
+
        self
    }
-
}

-
pub struct InventoryLock {
-
    inner: Inventory,
-
}
-

-
impl std::ops::Deref for InventoryLock {
-
    type Target = BTreeSet<RepoId>;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.inner
+
    pub fn empty() -> Self {
+
        Self::new(Vec::new())
    }
}

impl ReadStorage for MockStorage {
    type Repository = MockRepository;
-
    type InventoryRef<'a> = InventoryLock;

    fn info(&self) -> &git::UserInfo {
        &self.info
@@ -88,18 +82,6 @@ impl ReadStorage for MockStorage {
        Ok(self.repos.contains_key(rid))
    }

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        Ok(self.repos.keys().cloned().collect::<BTreeSet<_>>())
-
    }
-

-
    fn inventory_ref<'a, 's: 'a>(&'s self) -> Self::InventoryRef<'a> {
-
        InventoryLock {
-
            inner: self.inventory().unwrap(),
-
        }
-
    }
-

-
    fn insert(&self, _rid: RepoId) {}
-

    fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError> {
        self.repos
            .get(&rid)