Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Update `radicle-httpd` to latest `radicle` crate
Draft did:key:z6MkkfM3...sVz5 opened 1 year ago
  • Bump httpd version numbers to 0.18.0

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http

👉 Preview 👉 Workflow runs 👉 Branch on GitHub

18 files changed +98 -84 544cdca3 bf78db52
modified config/custom-environment-variables.json
@@ -1,7 +1,7 @@
{
  "nodes": {
    "fallbackPublicExplorer": "FALLBACK_PUBLIC_EXPLORER",
-
    "apiVersion": "API_VERSION",
+
    "requiredApiVersion": "REQUIRED_API_VERSION",
    "defaultHttpdPort": "DEFAULT_HTTPD_PORT",
    "defaultHttpdScheme": "DEFAULT_HTTPD_SCHEME"
  },
@@ -9,6 +9,7 @@
    "commitsPerPage": "COMMITS_PER_PAGE"
  },
  "supportWebsite": "SUPPORT_WEBSITE",
+
  "deploymentId": "VERCEL_DEPLOYMENT_ID",
  "fallbackPreferredSeed": {
    "__name": "PREFERRED_SEEDS",
    "__format": "json"
modified config/default.json
@@ -1,7 +1,7 @@
{
  "nodes": {
    "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
-
    "requiredApiVersion": "^6.0.0",
+
    "requiredApiVersion": "~0.18.0",
    "defaultHttpdPort": 443,
    "defaultHttpdScheme": "https"
  },
@@ -9,6 +9,7 @@
    "commitsPerPage": 30
  },
  "supportWebsite": "https://radicle.zulipchat.com",
+
  "deploymentId": null,
  "preferredSeeds": [
    {
      "hostname": "ash.radicle.garden",
modified http-client/lib/fetcher.ts
@@ -62,14 +62,17 @@ export class ResponseParseError extends Error {

    let description: string;
    if (!satisfies(nodeApiVersion, explorerRequiredApiVersion)) {
-
      description = `The node you are fetching from (v${nodeApiVersion}) doesn't match the version requirements of the web client ${explorerRequiredApiVersion}.`;
+
      description = `The node you are fetching from (v${nodeApiVersion}) doesn't match the version requirements of <code>radicle-explorer</code> ${explorerRequiredApiVersion}.`;
    } else {
-
      description = `The node (v${nodeApiVersion}) matches the version requirement of the web client (${explorerRequiredApiVersion}), but the web client isn't able to parse the response.`;
+
      description = `The node (v${nodeApiVersion}) matches the version requirement of <code>radicle-explorer</code> (${explorerRequiredApiVersion}), but <code>radicle-explorer</code> isn't able to parse the response.`;
    }
    this.apiVersion = apiVersion;
    this.description =
      "The response received from the seed does not match the expected schema.<br/>".concat(
        description,
+
        config.deploymentId
+
          ? ""
+
          : "<br/>If you are self-hosting <code>radicle-explorer</code> and run into this error, try to clear the browser's <code>localStorage</code> and the cache.",
      );

    this.method = method;
@@ -113,9 +116,14 @@ export class Fetcher {
    params: FetchParams,
    schema: T,
  ): Promise<TypeOf<T>> {
+
    if (config.deploymentId) {
+
      params.query ||= {};
+
      params.query["deployment_id"] = config.deploymentId;
+
    }
+

    const response = await this.fetch({
      ...params,
-
      query: { ...params.query, v: config.nodes.requiredApiVersion },
+
      path: `api/v1${params.path ? `/${params.path}` : ""}`,
    });

    if (!response.ok) {
@@ -138,7 +146,7 @@ export class Fetcher {
      throw new ResponseParseError(
        params.method,
        responseBody,
-
        info.apiVersion,
+
        info.version,
        result.error.errors,
        params.path,
      );
@@ -159,7 +167,7 @@ export class Fetcher {

    const pathSegment = path === undefined ? "" : `/${path}`;

-
    let url = `${this.#baseUrl.scheme}://${this.#baseUrl.hostname}:${this.#baseUrl.port}/api/v1${pathSegment}`;
+
    let url = `${this.#baseUrl.scheme}://${this.#baseUrl.hostname}:${this.#baseUrl.port}${pathSegment}`;

    if (query) {
      const searchparams = new URLSearchParams(query as Record<string, string>);
modified module.d.ts
@@ -10,6 +10,7 @@ declare module "virtual:*" {
    source: {
      commitsPerPage: number;
    };
+
    deploymentId: string | null;
    reactions: string[];
    supportWebsite: string;
    preferredSeeds: BaseUrl[];
modified radicle-httpd/Cargo.lock
@@ -1581,9 +1581,9 @@ dependencies = [

[[package]]
name = "radicle"
-
version = "0.13.0"
+
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "4a818569c11f1bac56f38b002d778ce8ec92e312024b9aebcd68bad5dee6a465"
+
checksum = "fd823aeed3ffe73eb82a213e62cb3811f9bdf453844d6e0b14684e0757fb389b"
dependencies = [
 "amplify",
 "base64 0.21.7",
@@ -1612,9 +1612,9 @@ dependencies = [

[[package]]
name = "radicle-cob"
-
version = "0.12.0"
+
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "d4fac94999d8ffb6e88674bee487b080b69bbc9fb1b439ebfa51481ede1a17b3"
+
checksum = "90581a9508ccc310998e991d7acf139d2991297d3fb37d30de07536e10256afb"
dependencies = [
 "fastrand",
 "git2",
@@ -1652,9 +1652,9 @@ dependencies = [

[[package]]
name = "radicle-dag"
-
version = "0.9.0"
+
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c2a678c3049a88ae6a34dd9f52ea9a5f9f066a0af63466b75cf8c48840303067"
+
checksum = "cb41c7e10ada3a4df960190a96bfb4af56d33ada890f917acc8e3b122b614875"
dependencies = [
 "fastrand",
]
@@ -1675,7 +1675,7 @@ dependencies = [

[[package]]
name = "radicle-httpd"
-
version = "0.17.1"
+
version = "0.18.0"
dependencies = [
 "anyhow",
 "axum",
modified radicle-httpd/Cargo.toml
@@ -3,7 +3,7 @@ name = "radicle-httpd"
description = "Radicle HTTP daemon"
homepage = "https://radicle.xyz"
license = "MIT OR Apache-2.0"
-
version = "0.17.1"
+
version = "0.18.0"
authors = ["cloudhead <cloudhead@radicle.xyz>"]
edition = "2021"
default-run = "radicle-httpd"
@@ -42,7 +42,7 @@ tracing-logfmt = { version = "0.3.5", optional = true }
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["std", "ansi", "fmt"] }

[dependencies.radicle]
-
version = "0.13.0"
+
version = "0.14.0"

[dev-dependencies]
hyper = { version = "1.4", default-features = false, features = ["client"] }
modified radicle-httpd/build/Dockerfile
@@ -35,7 +35,7 @@ RUN curl -sSf -o zig.tar.xz https://ziglang.org/download/0.12.0/zig-linu
    xz -d -c zig.tar.xz | tar -x && \
    mv zig-linux-x86_64-0.12.0/zig /usr/bin/zig && \
    mv zig-linux-x86_64-0.12.0/lib /usr/lib/zig && \
-
    cargo install cargo-zigbuild@0.18.3
+
    cargo install cargo-zigbuild@0.18.3 --locked


# Parts of the macOS SDK are required to build Radicle, we make these available
modified radicle-httpd/src/api.rs
@@ -1,14 +1,10 @@
use std::collections::BTreeMap;
use std::sync::Arc;
-
use std::time::Duration;

-
use axum::http::header::CONTENT_TYPE;
-
use axum::http::Method;
use axum::response::{IntoResponse, Json};
use axum::routing::get;
use axum::Router;
use serde_json::{json, Value};
-
use tower_http::cors::{self, CorsLayer};

use radicle::identity::doc::PayloadId;
use radicle::identity::{DocAt, RepoId};
@@ -30,7 +26,8 @@ use crate::Options;

pub const RADICLE_VERSION: &str = env!("RADICLE_VERSION");
// This version has to be updated on every breaking change to the radicle-httpd API.
-
pub const API_VERSION: &str = "6.1.0";
+
// deprecated: will be removed in the next minor release.
+
pub const API_VERSION: &str = "6.2.0";

#[derive(Clone)]
pub struct Context {
@@ -56,18 +53,18 @@ impl Context {

        let aliases = self.profile.aliases();
        let delegates = doc
-
            .delegates
-
            .into_iter()
-
            .map(|did| json::Author::new(&did).as_json(&aliases))
+
            .delegates()
+
            .iter()
+
            .map(|did| json::Author::new(did).as_json(&aliases))
            .collect::<Vec<_>>();
        let db = &self.profile.database()?;
        let seeding = db.count(&rid).unwrap_or_default();

        let payloads: BTreeMap<PayloadId, Value> = doc
-
            .payload
-
            .into_iter()
+
            .payload()
+
            .iter()
            .filter_map(|(id, payload)| {
-
                if id == PayloadId::project() {
+
                if id == &PayloadId::project() {
                    let (_, head) = repo.head().ok()?;
                    let patches = self.profile.patches(repo).ok()?;
                    let patches = patches.counts().ok()?;
@@ -75,7 +72,7 @@ impl Context {
                    let issues = issues.counts().ok()?;

                    Some((
-
                        id,
+
                        id.clone(),
                        json!({
                            "data": payload,
                            "meta": {
@@ -86,7 +83,7 @@ impl Context {
                        }),
                    ))
                } else {
-
                    Some((id, json!({ "data": payload })))
+
                    Some((id.clone(), json!({ "data": payload })))
                }
            })
            .collect();
@@ -94,8 +91,8 @@ impl Context {
        Ok(repo::Info {
            payloads,
            delegates,
-
            threshold: doc.threshold,
-
            visibility: doc.visibility,
+
            threshold: doc.threshold(),
+
            visibility: doc.visibility().clone(),
            rid,
            seeding,
        })
@@ -106,7 +103,7 @@ impl Context {
        let repo = self.profile.storage.repository(rid)?;
        let doc = repo.identity_doc()?;
        // Don't allow accessing private repos.
-
        if doc.visibility.is_private() {
+
        if doc.visibility().is_private() {
            return Err(Error::NotFound);
        }
        Ok((repo, doc))
@@ -122,13 +119,6 @@ pub fn router(ctx: Context) -> Router {
    Router::new()
        .route("/", get(root_handler))
        .merge(v1::router(ctx))
-
        .layer(
-
            CorsLayer::new()
-
                .max_age(Duration::from_secs(86400))
-
                .allow_origin(cors::Any)
-
                .allow_methods([Method::GET])
-
                .allow_headers([CONTENT_TYPE]),
-
        )
}

async fn root_handler() -> impl IntoResponse {
@@ -150,11 +140,9 @@ mod search {
    use std::cmp::Ordering;
    use std::collections::BTreeMap;

-
    use nonempty::NonEmpty;
    use serde::{Deserialize, Serialize};
    use serde_json::json;

-
    use radicle::crypto::Verified;
    use radicle::identity::doc::{Payload, PayloadId};
    use radicle::identity::RepoId;
    use radicle::node::routing::Store;
@@ -174,7 +162,7 @@ mod search {
    pub struct SearchResult {
        pub rid: RepoId,
        pub payloads: BTreeMap<PayloadId, Payload>,
-
        pub delegates: NonEmpty<serde_json::Value>,
+
        pub delegates: Vec<serde_json::Value>,
        pub seeds: usize,
        #[serde(skip)]
        pub index: usize,
@@ -183,30 +171,35 @@ mod search {
    impl SearchResult {
        pub fn new(
            q: &str,
-
            info: RepositoryInfo<Verified>,
+
            info: RepositoryInfo,
            db: &Database,
            aliases: &Aliases,
        ) -> Option<Self> {
-
            if info.doc.visibility.is_private() {
+
            if info.doc.visibility().is_private() {
                return None;
            }
            let Ok(Some(index)) = info.doc.project().map(|p| p.name().find(q)) else {
                return None;
            };
            let seeds = db.count(&info.rid).unwrap_or_default();
-
            let delegates = info.doc.delegates.map(|did| match aliases.alias(&did) {
-
                Some(alias) => json!({
-
                    "id": did,
-
                    "alias": alias,
-
                }),
-
                None => json!({
-
                    "id": did,
-
                }),
-
            });
+
            let delegates = info
+
                .doc
+
                .delegates()
+
                .iter()
+
                .map(|did| match aliases.alias(did) {
+
                    Some(alias) => json!({
+
                        "id": did,
+
                        "alias": alias,
+
                    }),
+
                    None => json!({
+
                        "id": did,
+
                    }),
+
                })
+
                .collect::<Vec<_>>();

            Some(SearchResult {
                rid: info.rid,
-
                payloads: info.doc.payload,
+
                payloads: info.doc.payload().clone(),
                delegates,
                seeds,
                index,
modified radicle-httpd/src/api/v1.rs
@@ -30,7 +30,7 @@ async fn root_handler(State(ctx): State<Context>) -> impl IntoResponse {
    let response = json!({
        "message": "Welcome!",
        "service": "radicle-httpd",
-
        "version": format!("{}-{}", RADICLE_VERSION, env!("GIT_HEAD")),
+
        "version": RADICLE_VERSION,
        "apiVersion": API_VERSION,
        "nid": ctx.profile.public_key,
        "path": "/api/v1",
modified radicle-httpd/src/api/v1/delegates.rs
@@ -37,14 +37,14 @@ async fn delegates_repos_handler(
        RepoQuery::All => storage
            .repositories()?
            .into_iter()
-
            .filter(|repo| repo.doc.visibility.is_public())
-
            .filter(|repo| repo.doc.delegates.iter().any(|d| *d == did))
+
            .filter(|repo| repo.doc.visibility().is_public())
+
            .filter(|repo| repo.doc.delegates().iter().any(|d| *d == did))
            .collect::<Vec<_>>(),
        RepoQuery::Pinned => storage
            .repositories_by_id(pinned.repositories.iter())?
            .into_iter()
-
            .filter(|repo| repo.doc.visibility.is_public())
-
            .filter(|repo| repo.doc.delegates.iter().any(|d| *d == did))
+
            .filter(|repo| repo.doc.visibility().is_public())
+
            .filter(|repo| repo.doc.delegates().iter().any(|d| *d == did))
            .collect::<Vec<_>>(),
    };
    repos.sort_by_key(|p| p.rid);
modified radicle-httpd/src/api/v1/repos.rs
@@ -73,12 +73,12 @@ async fn repo_root_handler(
        RepoQuery::All => storage
            .repositories()?
            .into_iter()
-
            .filter(|repo| repo.doc.visibility.is_public())
+
            .filter(|repo| repo.doc.visibility().is_public())
            .collect::<Vec<_>>(),
        RepoQuery::Pinned => storage
            .repositories_by_id(pinned.repositories.iter())?
            .into_iter()
-
            .filter(|repo| repo.doc.visibility.is_public())
+
            .filter(|repo| repo.doc.visibility().is_public())
            .collect::<Vec<_>>(),
    };
    repos.sort_by_key(|p| p.rid);
@@ -436,7 +436,7 @@ async fn stats_tree_handler(
/// `GET /repos/:rid/remotes`
async fn remotes_handler(State(ctx): State<Context>, Path(rid): Path<RepoId>) -> impl IntoResponse {
    let (repo, doc) = ctx.repo(rid)?;
-
    let delegates = &doc.delegates;
+
    let delegates = doc.delegates();
    let aliases = &ctx.profile.aliases();
    let remotes = repo
        .remotes()?
@@ -478,7 +478,7 @@ async fn remote_handler(
    Path((rid, node_id)): Path<(RepoId, NodeId)>,
) -> impl IntoResponse {
    let (repo, doc) = ctx.repo(rid)?;
-
    let delegates = &doc.delegates;
+
    let delegates = doc.delegates();
    let remote = repo.remote(&node_id)?;
    let refs = remote
        .refs
modified radicle-httpd/src/git.rs
@@ -85,7 +85,7 @@ async fn git_http_backend(

    // Don't allow cloning of private repositories.
    let doc = profile.storage.repository(id)?.identity_doc()?;
-
    if doc.visibility.is_private() {
+
    if doc.visibility().is_private() {
        return Err(Error::NotFound);
    }

modified radicle-httpd/src/lib.rs
@@ -17,7 +17,11 @@ use axum::http::{Request, Response};
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{middleware, Json, Router};
+
use hyper::header::CONTENT_TYPE;
+
use hyper::Method;
use tokio::net::TcpListener;
+
use tower_http::cors;
+
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tracing::Span;

@@ -26,6 +30,8 @@ use radicle::Profile;

use tracing_extra::{tracing_middleware, ColoredStatus, Paint, RequestId, TracingInfo};

+
use crate::api::RADICLE_VERSION;
+

mod api;
mod axum_extra;
mod cache;
@@ -119,7 +125,14 @@ fn router(options: Options, profile: Profile) -> anyhow::Result<Router> {
        .route("/", get(root_index_handler))
        .merge(git_router)
        .nest("/api", api_router)
-
        .nest("/raw", raw_router);
+
        .nest("/raw", raw_router)
+
        .layer(
+
            CorsLayer::new()
+
                .max_age(Duration::from_secs(86400))
+
                .allow_origin(cors::Any)
+
                .allow_methods([Method::GET])
+
                .allow_headers([CONTENT_TYPE]),
+
        );

    Ok(app)
}
@@ -127,6 +140,7 @@ fn router(options: Options, profile: Profile) -> anyhow::Result<Router> {
async fn root_index_handler() -> impl IntoResponse {
    let response = serde_json::json!({
        "welcome": "Welcome to the radicle-httpd JSON API, this service doesn't serve the Radicle Explorer web client.",
+
        "version": RADICLE_VERSION,
        "path": "/",
        "links": [
            {
modified radicle-httpd/src/raw.rs
@@ -1,14 +1,12 @@
use std::sync::Arc;
-
use std::time::Duration;

use axum::extract::{Query, State};
-
use axum::http::{header, HeaderValue, Method, StatusCode};
+
use axum::http::{header, HeaderValue, StatusCode};
use axum::response::IntoResponse;
use axum::routing::get;
use axum::Router;
use hyper::HeaderMap;
use radicle_surf::blob::{Blob, BlobRef};
-
use tower_http::cors;

use radicle::prelude::RepoId;
use radicle::profile::Profile;
@@ -99,13 +97,6 @@ pub fn router(profile: Arc<Profile>) -> Router {
        .route("/:rid/head/*path", get(file_by_canonical_head_handler))
        .route("/:rid/blobs/:oid", get(file_by_oid_handler))
        .with_state(profile)
-
        .layer(
-
            cors::CorsLayer::new()
-
                .max_age(Duration::from_secs(86400))
-
                .allow_origin(cors::Any)
-
                .allow_methods([Method::GET])
-
                .allow_headers([header::CONTENT_TYPE]),
-
        )
}

async fn file_by_commit_handler(
@@ -116,7 +107,7 @@ async fn file_by_commit_handler(
    let repo = storage.repository(rid)?;

    // Don't allow downloading raw files for private repos.
-
    if repo.identity_doc()?.visibility.is_private() {
+
    if repo.identity_doc()?.visibility().is_private() {
        return Err(Error::NotFound);
    }

@@ -134,7 +125,7 @@ async fn file_by_canonical_head_handler(
    let repo = storage.repository(rid)?;

    // Don't allow downloading raw files for private repos.
-
    if repo.identity_doc()?.visibility.is_private() {
+
    if repo.identity_doc()?.visibility().is_private() {
        return Err(Error::NotFound);
    }

@@ -176,7 +167,7 @@ async fn file_by_oid_handler(
    let repo = storage.repository(rid)?;

    // Don't allow downloading raw files for private repos.
-
    if repo.identity_doc()?.visibility.is_private() {
+
    if repo.identity_doc()?.visibility().is_private() {
        return Err(Error::NotFound);
    }

modified radicle-httpd/src/test.rs
@@ -7,20 +7,21 @@ use std::sync::Arc;
use axum::body::{Body, Bytes};
use axum::http::{Method, Request};
use axum::Router;
-
use radicle::node::{Features, Timestamp, UserAgent};
use serde_json::Value;
use tower::ServiceExt;

+
use radicle::cob::migrate;
use radicle::cob::patch::MergeTarget;
use radicle::crypto::ssh::Keystore;
+
use radicle::crypto::test::signer::MockSigner;
use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git::{raw as git2, RefString};
use radicle::identity::{project, Visibility};
+
use radicle::node::{Features, Timestamp, UserAgent};
use radicle::profile::{env, Home};
use radicle::storage::ReadStorage;
use radicle::{node, profile};
use radicle::{Node, Storage};
-
use radicle_crypto::test::signer::MockSigner;

use crate::api::Context;

@@ -65,6 +66,10 @@ pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
        )
        .unwrap();

+
    // Migrate COBs cache.
+
    let mut cobs = home.cobs_db_mut().unwrap();
+
    cobs.migrate(migrate::ignore).unwrap();
+

    radicle::storage::git::transport::local::register(storage.clone());
    keystore.store(keypair.clone(), "radicle", None).unwrap();

modified tests/e2e/node.spec.ts
@@ -22,7 +22,7 @@ test("node metadata", async ({ page, peerManager }) => {
  await page.goto(peer.uiUrl());

  await expect(page.getByText(shortNodeRemote).first()).toBeVisible();
-
  await expect(page.getByText("/radicle:1.0.0/")).toBeVisible();
+
  await expect(page.getByText("/radicle:1.1.0-pre.4/")).toBeVisible();
});

test("node repos", async ({ page }) => {
modified tests/support/heartwood-release
@@ -1 +1 @@
-
1.0.0

\ No newline at end of file
+
1.1.0-pre.4

\ No newline at end of file
modified tests/support/radicle-httpd-release
@@ -1 +1 @@
-
0.17.0

\ No newline at end of file
+
0.18.0-pre

\ No newline at end of file