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",
|
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||||
"preferredSeeds": [],
|
"preferredSeeds": [],
|
||||||
|
"web": {
|
||||||
|
"pinned": {
|
||||||
|
"repositories": []
|
||||||
|
}
|
||||||
|
},
|
||||||
"cli": {
|
"cli": {
|
||||||
"hints": false
|
"hints": false
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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!([
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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!(
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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