Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Move tracking store to `radicle` crate
Alexis Sellier committed 3 years ago
commit cf8113765c8485379a1c91749359a890c434a0b5
parent cf80f246b387f7805dcfc806abda53d9c156725e
9 files changed +393 -402
modified radicle-node/Cargo.toml
@@ -39,7 +39,6 @@ thiserror = { version = "1" }
[dependencies.radicle]
path = "../radicle"
version = "0.2.0"
-
features = ["sql"]

[dependencies.radicle-term]
path = "../radicle-term"
modified radicle-node/src/service/tracking.rs
@@ -1,5 +1,3 @@
-
mod store;
-

use std::ops;

use log::{error, warn};
@@ -13,11 +11,10 @@ use radicle::storage::{Namespaces, ReadRepository as _, ReadStorage};
use crate::prelude::Id;
use crate::service::NodeId;

+
pub use crate::node::tracking::store::Config as Store;
+
pub use crate::node::tracking::store::Error;
pub use crate::node::tracking::{Alias, Node, Policy, Repo, Scope};

-
pub use store::Config as Store;
-
pub use store::Error;
-

#[derive(Debug, Error)]
pub enum NamespacesError {
    #[error("Failed to find tracking policy for {rid}")]
@@ -52,12 +49,12 @@ pub struct Config {
    /// Default scope, if a scope for a specific repository was not found.
    scope: Scope,
    /// Underlying configuration store.
-
    store: store::Config,
+
    store: Store,
}

impl Config {
    /// Create a new tracking configuration.
-
    pub fn new(policy: Policy, scope: Scope, store: store::Config) -> Self {
+
    pub fn new(policy: Policy, scope: Scope, store: Store) -> Self {
        Self {
            policy,
            scope,
@@ -143,7 +140,7 @@ impl Config {
}

impl ops::Deref for Config {
-
    type Target = store::Config;
+
    type Target = Store;

    fn deref(&self) -> &Self::Target {
        &self.store
deleted radicle-node/src/service/tracking/schema.sql
@@ -1,32 +0,0 @@
-
--
-
-- Service configuration schema.
-
--
-

-
-- Node tracking policy.
-
create table if not exists "node-policies" (
-
  -- Node ID.
-
  "id"                 text      primary key not null,
-
  -- Node alias. May override the alias announced by the node.
-
  "alias"              text      default '',
-
  -- Tracking policy for this node.
-
  "policy"             text      default 'track'
-
  --
-
) strict;
-

-
-- Repository tracking policy.
-
create table if not exists "repo-policies" (
-
  -- Repository ID.
-
  "id"                 text      primary key not null,
-
  -- Tracking scope for this repository.
-
  --
-
  -- Valid values are:
-
  --
-
  -- "trusted"         track repository delegates and remotes in the `node-policies` table.
-
  -- "delegates-only"  only track repository delegates.
-
  -- "all"             track all remotes.
-
  --
-
  "scope"              text      default 'trusted',
-
  -- Tracking policy for this repository.
-
  "policy"             text      default 'track'
-
  --
-
) strict;
deleted radicle-node/src/service/tracking/store.rs
@@ -1,354 +0,0 @@
-
#![allow(clippy::type_complexity)]
-
use std::path::Path;
-
use std::{fmt, io, ops::Not as _};
-

-
use sqlite as sql;
-
use thiserror::Error;
-

-
use crate::prelude::Id;
-
use crate::service::NodeId;
-

-
use super::{Node, Policy, Repo, Scope};
-

-
#[derive(Error, Debug)]
-
pub enum Error {
-
    /// I/O error.
-
    #[error("i/o error: {0}")]
-
    Io(#[from] io::Error),
-
    /// An Internal error.
-
    #[error("internal error: {0}")]
-
    Internal(#[from] sql::Error),
-
}
-

-
/// Tracking configuration.
-
pub struct Config {
-
    db: sql::Connection,
-
}
-

-
impl fmt::Debug for Config {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Config(..)")
-
    }
-
}
-

-
impl Config {
-
    const SCHEMA: &str = include_str!("schema.sql");
-

-
    /// Open a policy store at the given path. Creates a new store if it
-
    /// doesn't exist.
-
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
-
        let db = sql::Connection::open(path)?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self { db })
-
    }
-

-
    /// Create a new in-memory address book.
-
    pub fn memory() -> Result<Self, Error> {
-
        let db = sql::Connection::open(":memory:")?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self { db })
-
    }
-

-
    /// Track a node.
-
    pub fn track_node(&mut self, id: &NodeId, alias: Option<&str>) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `node-policies` (id, alias)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET alias = ?2 WHERE alias != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, alias.unwrap_or_default()))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Track a repository.
-
    pub fn track_repo(&mut self, id: &Id, scope: Scope) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `repo-policies` (id, scope)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET scope = ?2 WHERE scope != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, scope))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Set a node's tracking policy.
-
    pub fn set_node_policy(&mut self, id: &NodeId, policy: Policy) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `node-policies` (id, policy)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET policy = ?2 WHERE policy != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, policy))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Set a repository's tracking policy.
-
    pub fn set_repo_policy(&mut self, id: &Id, policy: Policy) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `repo-policies` (id, policy)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET policy = ?2 WHERE policy != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, policy))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Untrack a node.
-
    pub fn untrack_node(&mut self, id: &NodeId) -> Result<bool, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("DELETE FROM `node-policies` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Untrack a repository.
-
    pub fn untrack_repo(&mut self, id: &Id) -> Result<bool, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("DELETE FROM `repo-policies` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Check if a node is tracked.
-
    pub fn is_node_tracked(&self, id: &NodeId) -> Result<bool, Error> {
-
        Ok(matches!(
-
            self.node_entry(id)?,
-
            Some(Node {
-
                policy: Policy::Track,
-
                ..
-
            })
-
        ))
-
    }
-

-
    /// Check if a repository is tracked.
-
    pub fn is_repo_tracked(&self, id: &Id) -> Result<bool, Error> {
-
        Ok(matches!(
-
            self.repo_entry(id)?,
-
            Some(Repo {
-
                policy: Policy::Track,
-
                ..
-
            })
-
        ))
-
    }
-

-
    /// Get a node's tracking information.
-
    pub fn node_entry(&self, id: &NodeId) -> Result<Option<Node>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT alias, policy FROM `node-policies` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-

-
        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            let alias = row.read::<&str, _>("alias");
-
            let alias = alias.is_empty().not().then_some(alias.to_owned());
-
            let policy = row.read::<Policy, _>("policy");
-

-
            return Ok(Some(Node {
-
                id: *id,
-
                alias,
-
                policy,
-
            }));
-
        }
-
        Ok(None)
-
    }
-

-
    /// Get a repository's tracking information.
-
    pub fn repo_entry(&self, id: &Id) -> Result<Option<Repo>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT scope, policy FROM `repo-policies` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-

-
        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            return Ok(Some(Repo {
-
                id: *id,
-
                scope: row.read::<Scope, _>("scope"),
-
                policy: row.read::<Policy, _>("policy"),
-
            }));
-
        }
-
        Ok(None)
-
    }
-

-
    /// Get node tracking entries.
-
    pub fn node_entries(&self) -> Result<Box<dyn Iterator<Item = Node>>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT id, alias, policy FROM `node-policies`")?
-
            .into_iter();
-
        let mut entries = Vec::new();
-

-
        while let Some(Ok(row)) = stmt.next() {
-
            let id = row.read("id");
-
            let alias = row.read::<&str, _>("alias").to_owned();
-
            let alias = alias.is_empty().not().then_some(alias.to_owned());
-
            let policy = row.read::<Policy, _>("policy");
-

-
            entries.push(Node { id, alias, policy });
-
        }
-
        Ok(Box::new(entries.into_iter()))
-
    }
-

-
    // TODO: see if sql can return iterator directly
-
    /// Get repository tracking entries.
-
    pub fn repo_entries(&self) -> Result<Box<dyn Iterator<Item = Repo>>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT id, scope, policy FROM `repo-policies`")?
-
            .into_iter();
-
        let mut entries = Vec::new();
-

-
        while let Some(Ok(row)) = stmt.next() {
-
            let id = row.read("id");
-
            let scope = row.read("scope");
-
            let policy = row.read::<Policy, _>("policy");
-

-
            entries.push(Repo { id, scope, policy });
-
        }
-
        Ok(Box::new(entries.into_iter()))
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use radicle::assert_matches;
-

-
    use super::*;
-
    use crate::test::arbitrary;
-

-
    #[test]
-
    fn test_track_and_untrack_node() {
-
        let id = arbitrary::gen::<NodeId>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_node(&id, Some("eve")).unwrap());
-
        assert!(db.is_node_tracked(&id).unwrap());
-
        assert!(!db.track_node(&id, Some("eve")).unwrap());
-
        assert!(db.untrack_node(&id).unwrap());
-
        assert!(!db.is_node_tracked(&id).unwrap());
-
    }
-

-
    #[test]
-
    fn test_track_and_untrack_repo() {
-
        let id = arbitrary::gen::<Id>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_repo(&id, Scope::All).unwrap());
-
        assert!(db.is_repo_tracked(&id).unwrap());
-
        assert!(!db.track_repo(&id, Scope::All).unwrap());
-
        assert!(db.untrack_repo(&id).unwrap());
-
        assert!(!db.is_repo_tracked(&id).unwrap());
-
    }
-

-
    #[test]
-
    fn test_node_entries() {
-
        let ids = arbitrary::vec::<NodeId>(3);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        for id in &ids {
-
            assert!(db.track_node(id, None).unwrap());
-
        }
-
        let mut entries = db.node_entries().unwrap();
-
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[0]);
-
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[1]);
-
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[2]);
-
    }
-

-
    #[test]
-
    fn test_repo_entries() {
-
        let ids = arbitrary::vec::<Id>(3);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        for id in &ids {
-
            assert!(db.track_repo(id, Scope::All).unwrap());
-
        }
-
        let mut entries = db.repo_entries().unwrap();
-
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[0]);
-
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[1]);
-
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[2]);
-
    }
-

-
    #[test]
-
    fn test_update_alias() {
-
        let id = arbitrary::gen::<NodeId>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_node(&id, Some("eve")).unwrap());
-
        assert_eq!(
-
            db.node_entry(&id).unwrap().unwrap().alias,
-
            Some(String::from("eve"))
-
        );
-
        assert!(db.track_node(&id, None).unwrap());
-
        assert_eq!(db.node_entry(&id).unwrap().unwrap().alias, None);
-
        assert!(!db.track_node(&id, None).unwrap());
-
        assert!(db.track_node(&id, Some("alice")).unwrap());
-
        assert_eq!(
-
            db.node_entry(&id).unwrap().unwrap().alias,
-
            Some(String::from("alice"))
-
        );
-
    }
-

-
    #[test]
-
    fn test_update_scope() {
-
        let id = arbitrary::gen::<Id>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_repo(&id, Scope::All).unwrap());
-
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().scope, Scope::All);
-
        assert!(db.track_repo(&id, Scope::Trusted).unwrap());
-
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().scope, Scope::Trusted);
-
    }
-

-
    #[test]
-
    fn test_repo_policy() {
-
        let id = arbitrary::gen::<Id>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_repo(&id, Scope::All).unwrap());
-
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().policy, Policy::Track);
-
        assert!(db.set_repo_policy(&id, Policy::Block).unwrap());
-
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().policy, Policy::Block);
-
    }
-

-
    #[test]
-
    fn test_node_policy() {
-
        let id = arbitrary::gen::<NodeId>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_node(&id, None).unwrap());
-
        assert_eq!(db.node_entry(&id).unwrap().unwrap().policy, Policy::Track);
-
        assert!(db.set_node_policy(&id, Policy::Block).unwrap());
-
        assert_eq!(db.node_entry(&id).unwrap().unwrap().policy, Policy::Block);
-
    }
-
}
modified radicle/Cargo.toml
@@ -8,7 +8,6 @@ edition = "2021"
[features]
default = []
test = ["qcheck", "radicle-crypto/test"]
-
sql = ["sqlite"]

[dependencies]
amplify = { version = "4.0.0-beta.7", default-features = false, features = ["std"] }
@@ -24,7 +23,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
siphasher = { version = "0.3.10" }
radicle-git-ext = { version = "0", features = ["serde"] }
-
sqlite = { version = "0.30.3", optional = true }
+
sqlite = { version = "0.30.3" }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }
unicode-normalization = { version = "0.1" }
modified radicle/src/lib.rs
@@ -17,7 +17,6 @@ pub mod node;
pub mod profile;
pub mod rad;
pub mod serde_ext;
-
#[cfg(feature = "sql")]
pub mod sql;
pub mod storage;
#[cfg(any(test, feature = "test"))]
modified radicle/src/node/tracking.rs
@@ -1,3 +1,5 @@
+
pub mod store;
+

use std::fmt;
use std::str::FromStr;

@@ -56,7 +58,6 @@ impl FromStr for Policy {
    }
}

-
#[cfg(feature = "sql")]
impl sqlite::BindableWithIndex for Policy {
    fn bind<I: sqlite::ParameterIndex>(
        self,
@@ -71,7 +72,6 @@ impl sqlite::BindableWithIndex for Policy {
    }
}

-
#[cfg(feature = "sql")]
impl TryFrom<&sqlite::Value> for Policy {
    type Error = sqlite::Error;

@@ -124,7 +124,6 @@ impl FromStr for Scope {
    }
}

-
#[cfg(feature = "sql")]
impl sqlite::BindableWithIndex for Scope {
    fn bind<I: sqlite::ParameterIndex>(
        self,
@@ -139,7 +138,6 @@ impl sqlite::BindableWithIndex for Scope {
    }
}

-
#[cfg(feature = "sql")]
impl TryFrom<&sqlite::Value> for Scope {
    type Error = sqlite::Error;

added radicle/src/node/tracking/schema.sql
@@ -0,0 +1,32 @@
+
--
+
-- Service configuration schema.
+
--
+

+
-- Node tracking policy.
+
create table if not exists "node-policies" (
+
  -- Node ID.
+
  "id"                 text      primary key not null,
+
  -- Node alias. May override the alias announced by the node.
+
  "alias"              text      default '',
+
  -- Tracking policy for this node.
+
  "policy"             text      default 'track'
+
  --
+
) strict;
+

+
-- Repository tracking policy.
+
create table if not exists "repo-policies" (
+
  -- Repository ID.
+
  "id"                 text      primary key not null,
+
  -- Tracking scope for this repository.
+
  --
+
  -- Valid values are:
+
  --
+
  -- "trusted"         track repository delegates and remotes in the `node-policies` table.
+
  -- "delegates-only"  only track repository delegates.
+
  -- "all"             track all remotes.
+
  --
+
  "scope"              text      default 'trusted',
+
  -- Tracking policy for this repository.
+
  "policy"             text      default 'track'
+
  --
+
) strict;
added radicle/src/node/tracking/store.rs
@@ -0,0 +1,353 @@
+
#![allow(clippy::type_complexity)]
+
use std::path::Path;
+
use std::{fmt, io, ops::Not as _};
+

+
use sqlite as sql;
+
use thiserror::Error;
+

+
use crate::prelude::{Id, NodeId};
+

+
use super::{Node, Policy, Repo, Scope};
+

+
#[derive(Error, Debug)]
+
pub enum Error {
+
    /// I/O error.
+
    #[error("i/o error: {0}")]
+
    Io(#[from] io::Error),
+
    /// An Internal error.
+
    #[error("internal error: {0}")]
+
    Internal(#[from] sql::Error),
+
}
+

+
/// Tracking configuration.
+
pub struct Config {
+
    db: sql::Connection,
+
}
+

+
impl fmt::Debug for Config {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Config(..)")
+
    }
+
}
+

+
impl Config {
+
    const SCHEMA: &str = include_str!("schema.sql");
+

+
    /// Open a policy store at the given path. Creates a new store if it
+
    /// doesn't exist.
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+
        let db = sql::Connection::open(path)?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self { db })
+
    }
+

+
    /// Create a new in-memory address book.
+
    pub fn memory() -> Result<Self, Error> {
+
        let db = sql::Connection::open(":memory:")?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self { db })
+
    }
+

+
    /// Track a node.
+
    pub fn track_node(&mut self, id: &NodeId, alias: Option<&str>) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `node-policies` (id, alias)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET alias = ?2 WHERE alias != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, alias.unwrap_or_default()))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Track a repository.
+
    pub fn track_repo(&mut self, id: &Id, scope: Scope) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `repo-policies` (id, scope)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET scope = ?2 WHERE scope != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, scope))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Set a node's tracking policy.
+
    pub fn set_node_policy(&mut self, id: &NodeId, policy: Policy) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `node-policies` (id, policy)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET policy = ?2 WHERE policy != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, policy))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Set a repository's tracking policy.
+
    pub fn set_repo_policy(&mut self, id: &Id, policy: Policy) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `repo-policies` (id, policy)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET policy = ?2 WHERE policy != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, policy))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Untrack a node.
+
    pub fn untrack_node(&mut self, id: &NodeId) -> Result<bool, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("DELETE FROM `node-policies` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Untrack a repository.
+
    pub fn untrack_repo(&mut self, id: &Id) -> Result<bool, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("DELETE FROM `repo-policies` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Check if a node is tracked.
+
    pub fn is_node_tracked(&self, id: &NodeId) -> Result<bool, Error> {
+
        Ok(matches!(
+
            self.node_entry(id)?,
+
            Some(Node {
+
                policy: Policy::Track,
+
                ..
+
            })
+
        ))
+
    }
+

+
    /// Check if a repository is tracked.
+
    pub fn is_repo_tracked(&self, id: &Id) -> Result<bool, Error> {
+
        Ok(matches!(
+
            self.repo_entry(id)?,
+
            Some(Repo {
+
                policy: Policy::Track,
+
                ..
+
            })
+
        ))
+
    }
+

+
    /// Get a node's tracking information.
+
    pub fn node_entry(&self, id: &NodeId) -> Result<Option<Node>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT alias, policy FROM `node-policies` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+

+
        if let Some(Ok(row)) = stmt.into_iter().next() {
+
            let alias = row.read::<&str, _>("alias");
+
            let alias = alias.is_empty().not().then_some(alias.to_owned());
+
            let policy = row.read::<Policy, _>("policy");
+

+
            return Ok(Some(Node {
+
                id: *id,
+
                alias,
+
                policy,
+
            }));
+
        }
+
        Ok(None)
+
    }
+

+
    /// Get a repository's tracking information.
+
    pub fn repo_entry(&self, id: &Id) -> Result<Option<Repo>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT scope, policy FROM `repo-policies` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+

+
        if let Some(Ok(row)) = stmt.into_iter().next() {
+
            return Ok(Some(Repo {
+
                id: *id,
+
                scope: row.read::<Scope, _>("scope"),
+
                policy: row.read::<Policy, _>("policy"),
+
            }));
+
        }
+
        Ok(None)
+
    }
+

+
    /// Get node tracking entries.
+
    pub fn node_entries(&self) -> Result<Box<dyn Iterator<Item = Node>>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT id, alias, policy FROM `node-policies`")?
+
            .into_iter();
+
        let mut entries = Vec::new();
+

+
        while let Some(Ok(row)) = stmt.next() {
+
            let id = row.read("id");
+
            let alias = row.read::<&str, _>("alias").to_owned();
+
            let alias = alias.is_empty().not().then_some(alias.to_owned());
+
            let policy = row.read::<Policy, _>("policy");
+

+
            entries.push(Node { id, alias, policy });
+
        }
+
        Ok(Box::new(entries.into_iter()))
+
    }
+

+
    // TODO: see if sql can return iterator directly
+
    /// Get repository tracking entries.
+
    pub fn repo_entries(&self) -> Result<Box<dyn Iterator<Item = Repo>>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT id, scope, policy FROM `repo-policies`")?
+
            .into_iter();
+
        let mut entries = Vec::new();
+

+
        while let Some(Ok(row)) = stmt.next() {
+
            let id = row.read("id");
+
            let scope = row.read("scope");
+
            let policy = row.read::<Policy, _>("policy");
+

+
            entries.push(Repo { id, scope, policy });
+
        }
+
        Ok(Box::new(entries.into_iter()))
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use crate::assert_matches;
+

+
    use super::*;
+
    use crate::test::arbitrary;
+

+
    #[test]
+
    fn test_track_and_untrack_node() {
+
        let id = arbitrary::gen::<NodeId>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_node(&id, Some("eve")).unwrap());
+
        assert!(db.is_node_tracked(&id).unwrap());
+
        assert!(!db.track_node(&id, Some("eve")).unwrap());
+
        assert!(db.untrack_node(&id).unwrap());
+
        assert!(!db.is_node_tracked(&id).unwrap());
+
    }
+

+
    #[test]
+
    fn test_track_and_untrack_repo() {
+
        let id = arbitrary::gen::<Id>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_repo(&id, Scope::All).unwrap());
+
        assert!(db.is_repo_tracked(&id).unwrap());
+
        assert!(!db.track_repo(&id, Scope::All).unwrap());
+
        assert!(db.untrack_repo(&id).unwrap());
+
        assert!(!db.is_repo_tracked(&id).unwrap());
+
    }
+

+
    #[test]
+
    fn test_node_entries() {
+
        let ids = arbitrary::vec::<NodeId>(3);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        for id in &ids {
+
            assert!(db.track_node(id, None).unwrap());
+
        }
+
        let mut entries = db.node_entries().unwrap();
+
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[0]);
+
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[1]);
+
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[2]);
+
    }
+

+
    #[test]
+
    fn test_repo_entries() {
+
        let ids = arbitrary::vec::<Id>(3);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        for id in &ids {
+
            assert!(db.track_repo(id, Scope::All).unwrap());
+
        }
+
        let mut entries = db.repo_entries().unwrap();
+
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[0]);
+
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[1]);
+
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[2]);
+
    }
+

+
    #[test]
+
    fn test_update_alias() {
+
        let id = arbitrary::gen::<NodeId>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_node(&id, Some("eve")).unwrap());
+
        assert_eq!(
+
            db.node_entry(&id).unwrap().unwrap().alias,
+
            Some(String::from("eve"))
+
        );
+
        assert!(db.track_node(&id, None).unwrap());
+
        assert_eq!(db.node_entry(&id).unwrap().unwrap().alias, None);
+
        assert!(!db.track_node(&id, None).unwrap());
+
        assert!(db.track_node(&id, Some("alice")).unwrap());
+
        assert_eq!(
+
            db.node_entry(&id).unwrap().unwrap().alias,
+
            Some(String::from("alice"))
+
        );
+
    }
+

+
    #[test]
+
    fn test_update_scope() {
+
        let id = arbitrary::gen::<Id>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_repo(&id, Scope::All).unwrap());
+
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().scope, Scope::All);
+
        assert!(db.track_repo(&id, Scope::Trusted).unwrap());
+
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().scope, Scope::Trusted);
+
    }
+

+
    #[test]
+
    fn test_repo_policy() {
+
        let id = arbitrary::gen::<Id>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_repo(&id, Scope::All).unwrap());
+
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().policy, Policy::Track);
+
        assert!(db.set_repo_policy(&id, Policy::Block).unwrap());
+
        assert_eq!(db.repo_entry(&id).unwrap().unwrap().policy, Policy::Block);
+
    }
+

+
    #[test]
+
    fn test_node_policy() {
+
        let id = arbitrary::gen::<NodeId>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_node(&id, None).unwrap());
+
        assert_eq!(db.node_entry(&id).unwrap().unwrap().policy, Policy::Track);
+
        assert!(db.set_node_policy(&id, Policy::Block).unwrap());
+
        assert_eq!(db.node_entry(&id).unwrap().unwrap().policy, Policy::Block);
+
    }
+
}