From 49584f4e732fb0039d2089f3c39fd56fc34a2ee3 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Mon, 22 Jan 2024 14:37:44 +0000 Subject: [PATCH] radicle: pinned repositories configuration Introduce pinned repositories to the `profile::Config`, i.e. `config.json`. This is used for pinning repositories in the httpd `/projects` handle. To allow returning all repositories, an `all` query parameter is also included. This is non-backwards compatible change, since we introduce the `show` query parameter which, if not supplied, will default to `pinned`. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- radicle-cli/examples/rad-config.md | 5 +++ radicle-httpd/src/api.rs | 10 ++++++ radicle-httpd/src/api/v1/delegates.rs | 47 +++++++++++++++++++-------- radicle-httpd/src/api/v1/profile.rs | 1 + radicle-httpd/src/api/v1/projects.rs | 33 ++++++++++++------- radicle-node/src/test/environment.rs | 3 +- radicle/src/cob/store.rs | 2 +- radicle/src/lib.rs | 1 + radicle/src/profile.rs | 6 +++- radicle/src/storage/git.rs | 18 ++++++++++ radicle/src/web.rs | 22 +++++++++++++ 11 files changed, 119 insertions(+), 29 deletions(-) create mode 100644 radicle/src/web.rs diff --git a/radicle-cli/examples/rad-config.md b/radicle-cli/examples/rad-config.md index 5fc6894d..b5051ba0 100644 --- a/radicle-cli/examples/rad-config.md +++ b/radicle-cli/examples/rad-config.md @@ -6,6 +6,11 @@ $ rad config { "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path", "preferredSeeds": [], + "web": { + "pinned": { + "repositories": [] + } + }, "cli": { "hints": false }, diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index b3708d81..75fb6218 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -125,10 +125,20 @@ async fn root_handler() -> impl IntoResponse { #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct PaginationQuery { + #[serde(default)] + pub show: ProjectQuery, pub page: Option, pub per_page: Option, } +#[derive(Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub enum ProjectQuery { + All, + #[default] + Pinned, +} + #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct RawQuery { diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index 260907c9..d7924df3 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -14,7 +14,7 @@ use radicle::storage::{ReadRepository, ReadStorage}; use crate::api::error::Error; use crate::api::project::Info; use crate::api::Context; -use crate::api::PaginationQuery; +use crate::api::{PaginationQuery, ProjectQuery}; use crate::axum_extra::{Path, Query}; pub fn router(ctx: Context) -> Router { @@ -34,19 +34,28 @@ async fn delegates_projects_handler( Path(delegate): Path, Query(qs): Query, ) -> impl IntoResponse { - let PaginationQuery { page, per_page } = qs; + let PaginationQuery { + show, + page, + per_page, + } = qs; let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let storage = &ctx.profile.storage; let db = &ctx.profile.database()?; - let mut projects = storage - .repositories()? - .into_iter() - .filter(|id| match &id.doc.visibility { - Visibility::Private { .. } => addr.ip().is_loopback(), - Visibility::Public => true, - }) - .collect::>(); + let pinned = &ctx.profile.config.web.pinned; + + let mut projects = match show { + ProjectQuery::All => storage + .repositories()? + .into_iter() + .filter(|repo| match &repo.doc.visibility { + Visibility::Private { .. } => addr.ip().is_loopback(), + Visibility::Public => true, + }) + .collect::>(), + ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?, + }; projects.sort_by_key(|p| p.rid); let infos = projects @@ -116,11 +125,16 @@ mod routes { .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080)))); let response = get( &app, - "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects", + "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all", ) .await; - assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.status(), + StatusCode::OK, + "failed response: {:?}", + response.json().await + ); assert_eq!( response.json().await, json!([ @@ -179,11 +193,16 @@ mod routes { )))); let response = get( &app, - "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects", + "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all", ) .await; - assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.status(), + StatusCode::OK, + "failed response: {:?}", + response.json().await + ); assert_eq!( response.json().await, json!([ diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs index ce703c0d..ccca4a40 100644 --- a/radicle-httpd/src/api/v1/profile.rs +++ b/radicle-httpd/src/api/v1/profile.rs @@ -75,6 +75,7 @@ mod routes { "preferredSeeds": [ "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776" ], + "web": { "pinned": { "repositories": [] } }, "cli": { "hints": true }, diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index ac4677fe..e10bfdb6 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -24,7 +24,7 @@ use radicle_surf::{diff, Glob, Oid, Repository}; use crate::api::error::Error; use crate::api::project::Info; -use crate::api::{self, announce_refs, CobsQuery, Context, PaginationQuery}; +use crate::api::{self, announce_refs, CobsQuery, Context, PaginationQuery, ProjectQuery}; use crate::axum_extra::{Path, Query}; const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate"; @@ -79,19 +79,28 @@ async fn project_root_handler( State(ctx): State, Query(qs): Query, ) -> impl IntoResponse { - let PaginationQuery { page, per_page } = qs; + let PaginationQuery { + show, + page, + per_page, + } = qs; let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let storage = &ctx.profile.storage; let db = &ctx.profile.database()?; - let mut projects = storage - .repositories()? - .into_iter() - .filter(|id| match &id.doc.visibility { - Visibility::Private { .. } => addr.ip().is_loopback(), - Visibility::Public => true, - }) - .collect::>(); + let pinned = &ctx.profile.config.web.pinned; + + let mut projects = match show { + ProjectQuery::All => storage + .repositories()? + .into_iter() + .filter(|repo| match &repo.doc.visibility { + Visibility::Private { .. } => addr.ip().is_loopback(), + Visibility::Public => true, + }) + .collect::>(), + ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?, + }; projects.sort_by_key(|p| p.rid); let infos = projects @@ -976,7 +985,7 @@ mod routes { let seed = seed(tmp.path()); let app = super::router(seed.clone()) .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080)))); - let response = get(&app, "/projects").await; + let response = get(&app, "/projects?show=all").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -1035,7 +1044,7 @@ mod routes { [192, 168, 13, 37], 8080, )))); - let response = get(&app, "/projects").await; + let response = get(&app, "/projects?show=all").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index a545bd5e..f811befc 100644 --- a/radicle-node/src/test/environment.rs +++ b/radicle-node/src/test/environment.rs @@ -13,7 +13,6 @@ use radicle::cob::issue; use radicle::crypto::ssh::{keystore::MemorySigner, Keystore}; use radicle::crypto::test::signer::MockSigner; use radicle::crypto::{KeyPair, Seed, Signer}; -use radicle::git; use radicle::git::refname; use radicle::identity::{RepoId, Visibility}; use radicle::node::config::ConnectAddress; @@ -30,6 +29,7 @@ use radicle::test::fixtures; use radicle::Storage; use radicle::{cli, node}; use radicle::{cob, explorer}; +use radicle::{git, web}; use crate::node::NodeId; use crate::service::Event; @@ -92,6 +92,7 @@ impl Environment { cli: cli::Config { hints: false }, public_explorer: explorer::Explorer::default(), preferred_seeds: PreferredSeeds::from(vec![]), + web: web::Config::default(), } } diff --git a/radicle/src/cob/store.rs b/radicle/src/cob/store.rs index ec21eaf6..c7c2e618 100644 --- a/radicle/src/cob/store.rs +++ b/radicle/src/cob/store.rs @@ -389,7 +389,7 @@ pub mod test { let obj = history.traverse(initial, &children, |mut acc, _, entry| { match Op::try_from(entry) { Ok(op) => { - if let Err(err) = acc.op(op, [].into_iter(), repo) { + if let Err(err) = acc.op(op, [], repo) { log::warn!("Error applying op to `{}` state: {err}", T::type_name()); return ControlFlow::Break(acc); } diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs index 4799ec32..47d99783 100644 --- a/radicle/src/lib.rs +++ b/radicle/src/lib.rs @@ -28,6 +28,7 @@ pub mod storage; #[cfg(any(test, feature = "test"))] pub mod test; pub mod version; +pub mod web; pub use cob::{issue, patch}; pub use node::Node; diff --git a/radicle/src/profile.rs b/radicle/src/profile.rs index 1c9caad5..245eb4b4 100644 --- a/radicle/src/profile.rs +++ b/radicle/src/profile.rs @@ -27,7 +27,7 @@ use crate::prelude::Did; use crate::prelude::NodeId; use crate::storage::git::transport; use crate::storage::git::Storage; -use crate::{cli, git, node}; +use crate::{cli, git, node, web}; /// Environment variables used by radicle. pub mod env { @@ -123,6 +123,9 @@ pub struct Config { /// and in other situations when a seed needs to be chosen. #[serde(default)] pub preferred_seeds: PreferredSeeds, + /// Web configuration. + #[serde(default)] + pub web: web::Config, /// CLI configuration. #[serde(default)] pub cli: cli::Config, @@ -136,6 +139,7 @@ impl Config { Self { public_explorer: Explorer::default(), preferred_seeds: PreferredSeeds::default(), + web: web::Config::default(), cli: cli::Config::default(), node: node::Config::new(alias), } diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index a9aa5284..b4e04806 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -252,6 +252,24 @@ impl Storage { Ok(repos) } + pub fn repositories_by_id<'a>( + &self, + mut rids: impl Iterator, + ) -> Result>, RepositoryError> { + rids.try_fold(Vec::new(), |mut infos, rid| { + let repo = self.repository(*rid)?; + let (_, head) = repo.head()?; + let info = RepositoryInfo { + rid: *rid, + head, + doc: repo.identity_doc()?.into(), + refs: refs::SignedRefsAt::load(self.info.key, &repo)?, + }; + infos.push(info); + Ok(infos) + }) + } + pub fn inspect(&self) -> Result<(), Error> { for r in self.repositories()? { let rid = r.rid; diff --git a/radicle/src/web.rs b/radicle/src/web.rs new file mode 100644 index 00000000..90988019 --- /dev/null +++ b/radicle/src/web.rs @@ -0,0 +1,22 @@ +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; + +use crate::prelude::RepoId; + +/// Web configuration. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Config { + /// Pinned content. + pub pinned: Pinned, +} + +/// Pinned content. This can be used to pin certain content when +/// listing, e.g. pin repositories on a web client. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Pinned { + /// Pinned repositories. + pub repositories: HashSet, +}