Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Migrate `/projects`: `root`, `project`, `commit`, `activity` handlers
xphoniex committed 3 years ago
commit de1f84b7927eafd653c1cb8439bae7d4c7089ee5
parent 2d49f8bbff7a498e9305f7efd634451a62a49074
6 files changed +190 -23
modified radicle-httpd/Cargo.toml
@@ -18,6 +18,7 @@ logfmt = [
anyhow = { version = "1" }
axum = { version = "0.5.16", default-features = false, features = ["json", "headers", "query"] }
axum-server = { version = "0.4.2", default-features = false }
+
chrono = { version = "0.4.22" }
ethers-core = { version = "1.0" }
fastrand = { version = "1.7.0" }
flate2 = { version = "1" }
@@ -29,7 +30,7 @@ siwe = { version = "0.5" }
thiserror = { version = "1" }
time = { version = "0.3.17" }
tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] }
-
tower-http = { version = "0.3.4", default-features = false, features = ["trace", "cors"] }
+
tower-http = { version = "0.3.4", default-features = false, features = ["trace", "cors", "set-header"] }
tracing = { version = "0.1.37", default-features = false, features = ["std", "log"] }
tracing-logfmt = { version = "0.2", optional = true }
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "ansi", "fmt"] }
@@ -37,3 +38,8 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["s
[dependencies.radicle]
path = "../radicle"
version = "0.2.0"
+

+
[dependencies.radicle-surf]
+
git = "https://github.com/radicle-dev/radicle-git"
+
features = ["serialize"]
+
rev = "d3115a22158c8395705babefdc89049f7510d32d"
modified radicle-httpd/src/api.rs
@@ -16,6 +16,9 @@ use tower_http::cors::{self, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::Span;

+
use radicle::cob::issue::Issues;
+
use radicle::identity::{Doc, Id};
+
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::Profile;

mod auth;
@@ -41,6 +44,22 @@ impl Context {
            sessions: Default::default(),
        }
    }
+

+
    pub fn project_info(&self, id: Id) -> Result<project::Info, error::Error> {
+
        let storage = &self.profile.storage;
+
        let repo = storage.repository(id)?;
+
        let (_, head) = repo.head()?;
+
        let Doc { payload, .. } = repo.project_of(self.profile.id())?;
+
        let issues = (Issues::open(self.profile.public_key, &repo)?).count()?;
+

+
        Ok(project::Info {
+
            payload,
+
            head,
+
            issues,
+
            patches: 0,
+
            id,
+
        })
+
    }
}

pub fn router(ctx: Context) -> Router {
@@ -115,3 +134,23 @@ pub struct PaginationQuery {
    pub page: Option<usize>,
    pub per_page: Option<usize>,
}
+

+
mod project {
+
    use radicle::git::Oid;
+
    use radicle::identity::project::Payload;
+
    use radicle::identity::Id;
+
    use serde::Serialize;
+

+
    /// Project info.
+
    #[derive(Serialize)]
+
    #[serde(rename_all = "camelCase")]
+
    pub struct Info {
+
        /// Project metadata.
+
        #[serde(flatten)]
+
        pub payload: Payload,
+
        pub head: Oid,
+
        pub patches: usize,
+
        pub issues: usize,
+
        pub id: Id,
+
    }
+
}
modified radicle-httpd/src/api/error.rs
@@ -42,7 +42,19 @@ pub enum Error {

    /// Storage error.
    #[error(transparent)]
-
    StorageError(#[from] radicle::storage::Error),
+
    Storage(#[from] radicle::storage::Error),
+

+
    /// Cob store error.
+
    #[error(transparent)]
+
    CobStore(#[from] radicle::cob::store::Error),
+

+
    /// Git project error.
+
    #[error(transparent)]
+
    GitProject(#[from] radicle::storage::git::ProjectError),
+

+
    /// Surf error.
+
    #[error(transparent)]
+
    SurfCommit(#[from] radicle_surf::commit::Error),
}

impl Error {
modified radicle-httpd/src/api/v1.rs
@@ -1,5 +1,6 @@
mod delegates;
mod node;
+
mod projects;
mod sessions;

use axum::Router;
@@ -10,7 +11,8 @@ pub fn router(ctx: Context) -> Router {
    let routes = Router::new()
        .merge(node::router(ctx.clone()))
        .merge(sessions::router(ctx.clone()))
-
        .merge(delegates::router(ctx));
+
        .merge(delegates::router(ctx.clone()))
+
        .merge(projects::router(ctx));

    Router::new().nest("/v1", routes)
}
modified radicle-httpd/src/api/v1/delegates.rs
@@ -1,31 +1,17 @@
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Extension, Json, Router};
-
use serde::Serialize;

-
use radicle::cob::store::Store;
-
use radicle::git::Oid;
-
use radicle::identity::project::Payload;
+
use radicle::cob::issue::Issues;
use radicle::identity::{Did, Doc};
use radicle::storage::{ReadRepository, WriteStorage};

use crate::api::axum_extra::{Path, Query};
use crate::api::error::Error;
+
use crate::api::project::Info;
use crate::api::Context;
use crate::api::PaginationQuery;

-
/// Project info.
-
#[derive(Serialize)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Info {
-
    /// Project metadata.
-
    #[serde(flatten)]
-
    pub payload: Payload,
-
    pub head: Oid,
-
    pub patches: usize,
-
    pub issues: usize,
-
}
-

pub fn router(ctx: Context) -> Router {
    Router::new()
        .route(
@@ -58,15 +44,14 @@ async fn delegates_projects_handler(
                return None;
            }

-
            let Ok(cobs) = Store::open(ctx.profile.public_key, &repo) else { return None };
-
            let Ok(issues) = cobs.issues().count() else { return None };
-
            let Ok(patches) = cobs.patches().count() else { return None };
+
            let Ok(issues) = Issues::open(ctx.profile.public_key, &repo) else { return None };
+
            let Ok(issues) = (*issues).count() else { return None };

            Some(Info {
                payload,
                head,
                issues,
-
                patches,
+
                patches: 0,
                id,
            })
        })
added radicle-httpd/src/api/v1/projects.rs
@@ -0,0 +1,123 @@
+
use axum::handler::Handler;
+
use axum::http::{header, HeaderValue};
+
use axum::response::IntoResponse;
+
use axum::routing::get;
+
use axum::{Extension, Json, Router};
+
use hyper::StatusCode;
+
use serde_json::json;
+
use tower_http::set_header::SetResponseHeaderLayer;
+

+
use radicle::cob::issue::Issues;
+
use radicle::identity::{Doc, Id};
+
use radicle::storage::{Oid, ReadRepository, WriteRepository, WriteStorage};
+
use radicle_surf::git::History;
+

+
use crate::api::axum_extra::{Path, Query};
+
use crate::api::error::Error;
+
use crate::api::project::Info;
+
use crate::api::{Context, PaginationQuery};
+

+
const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";
+

+
pub fn router(ctx: Context) -> Router {
+
    Router::new()
+
        .route("/projects", get(project_root_handler))
+
        .route("/projects/:project", get(project_handler))
+
        .route("/projects/:project/commits/:sha", get(commit_handler))
+
        .route(
+
            "/projects/:project/activity",
+
            get(
+
                activity_handler.layer(SetResponseHeaderLayer::if_not_present(
+
                    header::CACHE_CONTROL,
+
                    HeaderValue::from_static(CACHE_1_HOUR),
+
                )),
+
            ),
+
        )
+
        .layer(Extension(ctx))
+
}
+

+
/// List all projects.
+
/// `GET /projects`
+
async fn project_root_handler(
+
    Extension(ctx): Extension<Context>,
+
    Query(qs): Query<PaginationQuery>,
+
) -> impl IntoResponse {
+
    let PaginationQuery { page, per_page } = qs;
+
    let page = page.unwrap_or(0);
+
    let per_page = per_page.unwrap_or(10);
+
    let storage = &ctx.profile.storage;
+
    let projects = storage
+
        .projects()?
+
        .into_iter()
+
        .filter_map(|id| {
+
            let Ok(repo) = storage.repository(id) else { return None };
+
            let Ok((_, head)) = repo.head() else { return None };
+
            let Ok(Doc { payload, .. }) = repo.project_of(ctx.profile.id()) else { return None };
+
            let Ok(issues) = Issues::open(ctx.profile.public_key, &repo) else { return None };
+
            let Ok(issues) = (*issues).count() else { return None };
+

+
            Some(Info {
+
                payload,
+
                head,
+
                issues,
+
                patches: 0,
+
                id,
+
            })
+
        })
+
        .skip(page * per_page)
+
        .take(per_page)
+
        .collect::<Vec<_>>();
+

+
    Ok::<_, Error>(Json(projects))
+
}
+

+
/// Get project metadata.
+
/// `GET /projects/:project`
+
async fn project_handler(
+
    Extension(ctx): Extension<Context>,
+
    Path(id): Path<Id>,
+
) -> impl IntoResponse {
+
    let info = ctx.project_info(id)?;
+

+
    Ok::<_, Error>(Json(info))
+
}
+

+
/// Get project commit.
+
/// `GET /projects/:project/commits/:sha`
+
async fn commit_handler(
+
    Extension(ctx): Extension<Context>,
+
    Path((project, sha)): Path<(Id, Oid)>,
+
) -> impl IntoResponse {
+
    let storage = &ctx.profile.storage;
+
    let repo = storage.repository(project)?;
+
    let commit = radicle_surf::commit(&repo.raw().into(), sha)?;
+

+
    Ok::<_, Error>(Json(json!(commit)))
+
}
+

+
/// Get project activity for the past year.
+
/// `GET /projects/:project/activity`
+
async fn activity_handler(
+
    Extension(ctx): Extension<Context>,
+
    Path(project): Path<Id>,
+
) -> impl IntoResponse {
+
    let current_date = chrono::Utc::now().timestamp();
+
    let one_year_ago = chrono::Duration::weeks(52);
+
    let storage = &ctx.profile.storage;
+
    let repo = storage.repository(project)?;
+
    let (_, head) = repo.head()?;
+
    let timestamps = History::new(repo.raw().into(), head)
+
        .unwrap()
+
        .filter_map(|a| {
+
            if let Ok(a) = a {
+
                let seconds = a.committer.time.seconds();
+
                if seconds > current_date - one_year_ago.num_seconds() {
+
                    return Some(seconds);
+
                }
+
            }
+
            None
+
        })
+
        .collect::<Vec<i64>>();
+

+
    Ok::<_, Error>((StatusCode::OK, Json(json!({ "activity": timestamps }))))
+
}