Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Migrate `/projects`: `root`, `project`, `commit`, `activity` handlers
xphoniex committed 3 years ago
commit dea7082a6f4b14a232a924ba577e85ca2ba8f6e4
parent 77e0a176bdffb2b2995334bca583f9caaa5846ad
9 files changed +373 -46
modified Cargo.lock
@@ -68,6 +68,12 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"

[[package]]
name = "arrayvec"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+

+
[[package]]
+
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
@@ -238,14 +244,26 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "bitvec"
+
version = "0.19.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
+
dependencies = [
+
 "funty 1.1.0",
+
 "radium 0.5.3",
+
 "tap",
+
 "wyz 0.2.0",
+
]
+

+
[[package]]
+
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
-
 "funty",
-
 "radium",
+
 "funty 2.0.0",
+
 "radium 0.7.0",
 "tap",
-
 "wyz",
+
 "wyz 0.5.1",
]

[[package]]
@@ -854,7 +872,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade3e9c97727343984e1ceada4fdab11142d2ee3472d2c67027d56b1251d4f15"
dependencies = [
-
 "arrayvec",
+
 "arrayvec 0.7.2",
 "bytes",
 "chrono",
 "elliptic-curve",
@@ -894,6 +912,18 @@ dependencies = [
]

[[package]]
+
name = "filetime"
+
version = "0.2.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3"
+
dependencies = [
+
 "cfg-if",
+
 "libc",
+
 "redox_syscall",
+
 "windows-sys",
+
]
+

+
[[package]]
name = "fixed-hash"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -938,6 +968,12 @@ dependencies = [

[[package]]
name = "funty"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+

+
[[package]]
+
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
@@ -1039,8 +1075,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d819e0a29c3201eb3373f9371be2bf821437979791ff07fe23a40ed8adadcfc3"
dependencies = [
-
 "git-ref-format-core",
-
 "git-ref-format-macro",
+
 "git-ref-format-core 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+
 "git-ref-format-macro 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+
]
+

+
[[package]]
+
name = "git-ref-format"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d"
+
dependencies = [
+
 "git-ref-format-core 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)",
+
 "git-ref-format-macro 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)",
]

[[package]]
@@ -1054,12 +1099,32 @@ dependencies = [
]

[[package]]
+
name = "git-ref-format-core"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d"
+
dependencies = [
+
 "serde",
+
 "thiserror",
+
]
+

+
[[package]]
name = "git-ref-format-macro"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66434445c9d45a85a9186a76770b1f80a44fdf8ed34fb6d4e3d12c5b9fc942ef"
dependencies = [
-
 "git-ref-format-core",
+
 "git-ref-format-core 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+
 "proc-macro-error",
+
 "quote",
+
 "syn",
+
]
+

+
[[package]]
+
name = "git-ref-format-macro"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d"
+
dependencies = [
+
 "git-ref-format-core 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)",
 "proc-macro-error",
 "quote",
 "syn",
@@ -1071,7 +1136,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0934f57135449b88bea0e28efd80aab0c1b53692f8207c7e232086db824c7a8"
dependencies = [
-
 "nom",
+
 "nom 7.1.1",
 "thiserror",
]

@@ -1500,6 +1565,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"

[[package]]
+
name = "lexical-core"
+
version = "0.7.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+
dependencies = [
+
 "arrayvec 0.5.2",
+
 "bitflags",
+
 "cfg-if",
+
 "ryu",
+
 "static_assertions",
+
]
+

+
[[package]]
name = "lexopt"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1671,6 +1749,19 @@ dependencies = [

[[package]]
name = "nom"
+
version = "6.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
+
dependencies = [
+
 "bitvec 0.19.6",
+
 "funty 1.1.0",
+
 "lexical-core",
+
 "memchr",
+
 "version_check",
+
]
+

+
[[package]]
+
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
@@ -1681,6 +1772,12 @@ dependencies = [

[[package]]
name = "nonempty"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9ff7ac1e5ea23db6d61ad103e91864675049644bf47c35912336352fa4e9c109"
+

+
[[package]]
+
name = "nonempty"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f1f8e5676e1a1f2ee8b21f38238e1243c827531c9435624c7bfb305102cee4"
@@ -1797,7 +1894,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce"
dependencies = [
-
 "arrayvec",
+
 "arrayvec 0.7.2",
 "auto_impl",
 "bytes",
 "ethereum-types",
@@ -1869,8 +1966,8 @@ version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "366e44391a8af4cfd6002ef6ba072bae071a96aafca98d7d448a34c5dca38b6a"
dependencies = [
-
 "arrayvec",
-
 "bitvec",
+
 "arrayvec 0.7.2",
+
 "bitvec 1.0.1",
 "byte-slice-cast",
 "impl-trait-for-tuples",
 "parity-scale-codec-derive",
@@ -2110,11 +2207,11 @@ dependencies = [
 "cyphernet",
 "ed25519-compact",
 "fastrand",
-
 "git-ref-format",
+
 "git-ref-format 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "git2",
 "log",
 "multibase",
-
 "nonempty",
+
 "nonempty 0.8.0",
 "olpc-cjson",
 "once_cell",
 "pretty_assertions",
@@ -2168,7 +2265,7 @@ dependencies = [
 "ed25519-compact",
 "fastrand",
 "git-commit",
-
 "git-ref-format",
+
 "git-ref-format 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "git-trailers",
 "git2",
 "log",
@@ -2206,7 +2303,7 @@ dependencies = [
 "cyphernet",
 "ed25519-compact",
 "fastrand",
-
 "git-ref-format",
+
 "git-ref-format 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "multibase",
 "quickcheck",
 "quickcheck_macros",
@@ -2223,10 +2320,9 @@ dependencies = [
[[package]]
name = "radicle-git-ext"
version = "0.2.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "25ed92fcf331d19b3110bbed8d3fe2bd99dc75f0059fd135727d24ef829de507"
+
source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d"
dependencies = [
-
 "git-ref-format",
+
 "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)",
 "git2",
 "percent-encoding",
 "radicle-std-ext",
@@ -2241,12 +2337,14 @@ dependencies = [
 "anyhow",
 "axum",
 "axum-server",
+
 "chrono",
 "ethers-core",
 "fastrand",
 "flate2",
 "hyper",
 "lexopt",
 "radicle",
+
 "radicle-surf",
 "serde",
 "serde_json",
 "siwe",
@@ -2270,12 +2368,12 @@ dependencies = [
 "colored",
 "crossbeam-channel",
 "fastrand",
-
 "git-ref-format",
+
 "git-ref-format 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "lexopt",
 "log",
 "nakamoto-net",
 "nakamoto-net-poll",
-
 "nonempty",
+
 "nonempty 0.8.0",
 "quickcheck",
 "quickcheck_macros",
 "radicle",
@@ -2311,20 +2409,45 @@ dependencies = [
[[package]]
name = "radicle-std-ext"
version = "0.1.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "db20136bbc9ae63f3fec8e5a6c369f4902fac2244501b5dfc6d668e43475aaa4"
+
source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d"
+

+
[[package]]
+
name = "radicle-surf"
+
version = "0.8.0"
+
source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d"
+
dependencies = [
+
 "anyhow",
+
 "base64",
+
 "either",
+
 "flate2",
+
 "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)",
+
 "git2",
+
 "nom 6.1.2",
+
 "nonempty 0.5.0",
+
 "radicle-git-ext",
+
 "regex",
+
 "serde",
+
 "tar",
+
 "thiserror",
+
]

[[package]]
name = "radicle-tools"
version = "0.2.0"
dependencies = [
 "anyhow",
-
 "git-ref-format",
+
 "git-ref-format 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "radicle",
]

[[package]]
name = "radium"
+
version = "0.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+

+
[[package]]
+
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
@@ -2986,6 +3109,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"

[[package]]
+
name = "tar"
+
version = "0.4.38"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
+
dependencies = [
+
 "filetime",
+
 "libc",
+
 "xattr",
+
]
+

+
[[package]]
name = "tempfile"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3616,6 +3750,12 @@ checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"

[[package]]
name = "wyz"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+

+
[[package]]
+
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
@@ -3624,6 +3764,15 @@ dependencies = [
]

[[package]]
+
name = "xattr"
+
version = "0.2.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified Cargo.toml
@@ -35,3 +35,7 @@ version = "0.3.0"
git = "https://github.com/internet2-wg/rust-cyphernet"
rev = "dee03a95abe4c964e5d9f8532c7dc76998dfeea7"
version = "0.1.0"
+

+
[patch.crates-io.radicle-git-ext]
+
git = "https://github.com/radicle-dev/radicle-git"
+
rev = "d3115a22158c8395705babefdc89049f7510d32d"
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 }))))
+
}
modified radicle/src/cob/store.rs
@@ -147,6 +147,13 @@ impl<'a, T: FromHistory> Store<'a, T> {
        }))
    }

+
    /// Return objects count.
+
    pub fn count(&self) -> Result<usize, Error> {
+
        let raw = cob::list(self.raw, T::type_name())?;
+

+
        Ok(raw.len())
+
    }
+

    pub fn remove(&self, _id: &ObjectId) -> Result<(), Error> {
        todo!();
    }