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 <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2024-01-22 14:37:44 +00:00 committed by cloudhead
parent d722638905
commit 49584f4e73
No known key found for this signature in database
11 changed files with 119 additions and 29 deletions

View File

@ -6,6 +6,11 @@ $ rad config
{ {
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path", "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
"preferredSeeds": [], "preferredSeeds": [],
"web": {
"pinned": {
"repositories": []
}
},
"cli": { "cli": {
"hints": false "hints": false
}, },

View File

@ -125,10 +125,20 @@ async fn root_handler() -> impl IntoResponse {
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PaginationQuery { pub struct PaginationQuery {
#[serde(default)]
pub show: ProjectQuery,
pub page: Option<usize>, pub page: Option<usize>,
pub per_page: Option<usize>, pub per_page: Option<usize>,
} }
#[derive(Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub enum ProjectQuery {
All,
#[default]
Pinned,
}
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RawQuery { pub struct RawQuery {

View File

@ -14,7 +14,7 @@ use radicle::storage::{ReadRepository, ReadStorage};
use crate::api::error::Error; use crate::api::error::Error;
use crate::api::project::Info; use crate::api::project::Info;
use crate::api::Context; use crate::api::Context;
use crate::api::PaginationQuery; use crate::api::{PaginationQuery, ProjectQuery};
use crate::axum_extra::{Path, Query}; use crate::axum_extra::{Path, Query};
pub fn router(ctx: Context) -> Router { pub fn router(ctx: Context) -> Router {
@ -34,19 +34,28 @@ async fn delegates_projects_handler(
Path(delegate): Path<Did>, Path(delegate): Path<Did>,
Query(qs): Query<PaginationQuery>, Query(qs): Query<PaginationQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let PaginationQuery { page, per_page } = qs; let PaginationQuery {
show,
page,
per_page,
} = qs;
let page = page.unwrap_or(0); let page = page.unwrap_or(0);
let per_page = per_page.unwrap_or(10); let per_page = per_page.unwrap_or(10);
let storage = &ctx.profile.storage; let storage = &ctx.profile.storage;
let db = &ctx.profile.database()?; let db = &ctx.profile.database()?;
let mut projects = storage let pinned = &ctx.profile.config.web.pinned;
.repositories()?
.into_iter() let mut projects = match show {
.filter(|id| match &id.doc.visibility { ProjectQuery::All => storage
Visibility::Private { .. } => addr.ip().is_loopback(), .repositories()?
Visibility::Public => true, .into_iter()
}) .filter(|repo| match &repo.doc.visibility {
.collect::<Vec<_>>(); Visibility::Private { .. } => addr.ip().is_loopback(),
Visibility::Public => true,
})
.collect::<Vec<_>>(),
ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?,
};
projects.sort_by_key(|p| p.rid); projects.sort_by_key(|p| p.rid);
let infos = projects let infos = projects
@ -116,11 +125,16 @@ mod routes {
.layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080)))); .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))));
let response = get( let response = get(
&app, &app,
"/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects", "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all",
) )
.await; .await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(
response.status(),
StatusCode::OK,
"failed response: {:?}",
response.json().await
);
assert_eq!( assert_eq!(
response.json().await, response.json().await,
json!([ json!([
@ -179,11 +193,16 @@ mod routes {
)))); ))));
let response = get( let response = get(
&app, &app,
"/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects", "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all",
) )
.await; .await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(
response.status(),
StatusCode::OK,
"failed response: {:?}",
response.json().await
);
assert_eq!( assert_eq!(
response.json().await, response.json().await,
json!([ json!([

View File

@ -75,6 +75,7 @@ mod routes {
"preferredSeeds": [ "preferredSeeds": [
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776" "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776"
], ],
"web": { "pinned": { "repositories": [] } },
"cli": { "cli": {
"hints": true "hints": true
}, },

View File

@ -24,7 +24,7 @@ use radicle_surf::{diff, Glob, Oid, Repository};
use crate::api::error::Error; use crate::api::error::Error;
use crate::api::project::Info; 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}; use crate::axum_extra::{Path, Query};
const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate"; const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";
@ -79,19 +79,28 @@ async fn project_root_handler(
State(ctx): State<Context>, State(ctx): State<Context>,
Query(qs): Query<PaginationQuery>, Query(qs): Query<PaginationQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let PaginationQuery { page, per_page } = qs; let PaginationQuery {
show,
page,
per_page,
} = qs;
let page = page.unwrap_or(0); let page = page.unwrap_or(0);
let per_page = per_page.unwrap_or(10); let per_page = per_page.unwrap_or(10);
let storage = &ctx.profile.storage; let storage = &ctx.profile.storage;
let db = &ctx.profile.database()?; let db = &ctx.profile.database()?;
let mut projects = storage let pinned = &ctx.profile.config.web.pinned;
.repositories()?
.into_iter() let mut projects = match show {
.filter(|id| match &id.doc.visibility { ProjectQuery::All => storage
Visibility::Private { .. } => addr.ip().is_loopback(), .repositories()?
Visibility::Public => true, .into_iter()
}) .filter(|repo| match &repo.doc.visibility {
.collect::<Vec<_>>(); Visibility::Private { .. } => addr.ip().is_loopback(),
Visibility::Public => true,
})
.collect::<Vec<_>>(),
ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?,
};
projects.sort_by_key(|p| p.rid); projects.sort_by_key(|p| p.rid);
let infos = projects let infos = projects
@ -976,7 +985,7 @@ mod routes {
let seed = seed(tmp.path()); let seed = seed(tmp.path());
let app = super::router(seed.clone()) let app = super::router(seed.clone())
.layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080)))); .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!(response.status(), StatusCode::OK);
assert_eq!( assert_eq!(
@ -1035,7 +1044,7 @@ mod routes {
[192, 168, 13, 37], [192, 168, 13, 37],
8080, 8080,
)))); ))));
let response = get(&app, "/projects").await; let response = get(&app, "/projects?show=all").await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
assert_eq!( assert_eq!(

View File

@ -13,7 +13,6 @@ use radicle::cob::issue;
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore}; use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
use radicle::crypto::test::signer::MockSigner; use radicle::crypto::test::signer::MockSigner;
use radicle::crypto::{KeyPair, Seed, Signer}; use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git;
use radicle::git::refname; use radicle::git::refname;
use radicle::identity::{RepoId, Visibility}; use radicle::identity::{RepoId, Visibility};
use radicle::node::config::ConnectAddress; use radicle::node::config::ConnectAddress;
@ -30,6 +29,7 @@ use radicle::test::fixtures;
use radicle::Storage; use radicle::Storage;
use radicle::{cli, node}; use radicle::{cli, node};
use radicle::{cob, explorer}; use radicle::{cob, explorer};
use radicle::{git, web};
use crate::node::NodeId; use crate::node::NodeId;
use crate::service::Event; use crate::service::Event;
@ -92,6 +92,7 @@ impl Environment {
cli: cli::Config { hints: false }, cli: cli::Config { hints: false },
public_explorer: explorer::Explorer::default(), public_explorer: explorer::Explorer::default(),
preferred_seeds: PreferredSeeds::from(vec![]), preferred_seeds: PreferredSeeds::from(vec![]),
web: web::Config::default(),
} }
} }

View File

@ -389,7 +389,7 @@ pub mod test {
let obj = history.traverse(initial, &children, |mut acc, _, entry| { let obj = history.traverse(initial, &children, |mut acc, _, entry| {
match Op::try_from(entry) { match Op::try_from(entry) {
Ok(op) => { 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()); log::warn!("Error applying op to `{}` state: {err}", T::type_name());
return ControlFlow::Break(acc); return ControlFlow::Break(acc);
} }

View File

@ -28,6 +28,7 @@ pub mod storage;
#[cfg(any(test, feature = "test"))] #[cfg(any(test, feature = "test"))]
pub mod test; pub mod test;
pub mod version; pub mod version;
pub mod web;
pub use cob::{issue, patch}; pub use cob::{issue, patch};
pub use node::Node; pub use node::Node;

View File

@ -27,7 +27,7 @@ use crate::prelude::Did;
use crate::prelude::NodeId; use crate::prelude::NodeId;
use crate::storage::git::transport; use crate::storage::git::transport;
use crate::storage::git::Storage; use crate::storage::git::Storage;
use crate::{cli, git, node}; use crate::{cli, git, node, web};
/// Environment variables used by radicle. /// Environment variables used by radicle.
pub mod env { pub mod env {
@ -123,6 +123,9 @@ pub struct Config {
/// and in other situations when a seed needs to be chosen. /// and in other situations when a seed needs to be chosen.
#[serde(default)] #[serde(default)]
pub preferred_seeds: PreferredSeeds, pub preferred_seeds: PreferredSeeds,
/// Web configuration.
#[serde(default)]
pub web: web::Config,
/// CLI configuration. /// CLI configuration.
#[serde(default)] #[serde(default)]
pub cli: cli::Config, pub cli: cli::Config,
@ -136,6 +139,7 @@ impl Config {
Self { Self {
public_explorer: Explorer::default(), public_explorer: Explorer::default(),
preferred_seeds: PreferredSeeds::default(), preferred_seeds: PreferredSeeds::default(),
web: web::Config::default(),
cli: cli::Config::default(), cli: cli::Config::default(),
node: node::Config::new(alias), node: node::Config::new(alias),
} }

View File

@ -252,6 +252,24 @@ impl Storage {
Ok(repos) Ok(repos)
} }
pub fn repositories_by_id<'a>(
&self,
mut rids: impl Iterator<Item = &'a RepoId>,
) -> Result<Vec<RepositoryInfo<Verified>>, 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> { pub fn inspect(&self) -> Result<(), Error> {
for r in self.repositories()? { for r in self.repositories()? {
let rid = r.rid; let rid = r.rid;

22
radicle/src/web.rs Normal file
View File

@ -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<RepoId>,
}