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:
parent
d722638905
commit
49584f4e73
|
|
@ -6,6 +6,11 @@ $ rad config
|
|||
{
|
||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||
"preferredSeeds": [],
|
||||
"web": {
|
||||
"pinned": {
|
||||
"repositories": []
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"hints": false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawQuery {
|
||||
|
|
|
|||
|
|
@ -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<Did>,
|
||||
Query(qs): Query<PaginationQuery>,
|
||||
) -> 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::<Vec<_>>();
|
||||
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::<Vec<_>>(),
|
||||
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!([
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ mod routes {
|
|||
"preferredSeeds": [
|
||||
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776"
|
||||
],
|
||||
"web": { "pinned": { "repositories": [] } },
|
||||
"cli": {
|
||||
"hints": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<Context>,
|
||||
Query(qs): Query<PaginationQuery>,
|
||||
) -> 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::<Vec<_>>();
|
||||
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::<Vec<_>>(),
|
||||
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!(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,6 +252,24 @@ impl Storage {
|
|||
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> {
|
||||
for r in self.repositories()? {
|
||||
let rid = r.rid;
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
Loading…
Reference in New Issue