rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwooda4989b7ce79590fdab3b16e41a8b6049fed3ed4b
{
"request": "trigger",
"version": 1,
"event_type": "push",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"pusher": {
"id": "did:key:z6Mkum88mKcX2sARx8R18RZUmAdakPFbXtdHzn8JNVEDqkxj",
"alias": "justarandomgeek-trantor"
},
"before": "a4989b7ce79590fdab3b16e41a8b6049fed3ed4b",
"after": "a4989b7ce79590fdab3b16e41a8b6049fed3ed4b",
"branch": "master",
"commits": [
"a4989b7ce79590fdab3b16e41a8b6049fed3ed4b"
]
}
{
"response": "triggered",
"run_id": {
"id": "e9a02dd5-6255-49c5-a910-3b567390b8bc"
},
"info_url": "https://cci.rad.levitte.org//e9a02dd5-6255-49c5-a910-3b567390b8bc.html"
}
Started at: 2025-10-21 20:52:39.600804+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/e9a02dd5-6255-49c5-a910-3b567390b8bc/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 125 issues · 15 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout a4989b7ce79590fdab3b16e41a8b6049fed3ed4b
HEAD is now at a4989b7c Remove `radicle-httpd` crate
Exit code: 0
$ git show a4989b7ce79590fdab3b16e41a8b6049fed3ed4b
commit a4989b7ce79590fdab3b16e41a8b6049fed3ed4b
Author: cloudhead <cloudhead@radicle.xyz>
Date: Thu Jun 6 11:14:00 2024 +0200
Remove `radicle-httpd` crate
The HTTP daemon is moved to the `radicle-explorer` repository. Hence,
all traces of it are removed from this repository.
diff --git a/Cargo.lock b/Cargo.lock
index ece6ea36..be29bfe2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 3
-[[package]]
-name = "addr2line"
-version = "0.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
-dependencies = [
- "gimli",
-]
-
[[package]]
name = "adler"
version = "1.0.2"
@@ -52,18 +43,6 @@ dependencies = [
"subtle",
]
-[[package]]
-name = "ahash"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
-dependencies = [
- "cfg-if",
- "once_cell",
- "version_check",
- "zerocopy",
-]
-
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -73,12 +52,6 @@ dependencies = [
"memchr",
]
-[[package]]
-name = "allocator-api2"
-version = "0.2.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
-
[[package]]
name = "amplify"
version = "4.6.0"
@@ -204,122 +177,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
-[[package]]
-name = "async-trait"
-version = "0.1.80"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.60",
-]
-
[[package]]
name = "autocfg"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
-[[package]]
-name = "axum"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
-dependencies = [
- "async-trait",
- "axum-core",
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-util",
- "itoa",
- "matchit",
- "memchr",
- "mime",
- "percent-encoding",
- "pin-project-lite",
- "rustversion",
- "serde",
- "serde_json",
- "serde_path_to_error",
- "serde_urlencoded",
- "sync_wrapper 1.0.1",
- "tokio",
- "tower",
- "tower-layer",
- "tower-service",
-]
-
-[[package]]
-name = "axum-auth"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8169113a185f54f68614fcfc3581df585d30bf8542bcb99496990e1025e4120a"
-dependencies = [
- "async-trait",
- "axum-core",
- "base64 0.21.7",
- "http",
-]
-
-[[package]]
-name = "axum-core"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
-dependencies = [
- "async-trait",
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "mime",
- "pin-project-lite",
- "rustversion",
- "sync_wrapper 0.1.2",
- "tower-layer",
- "tower-service",
-]
-
-[[package]]
-name = "axum-server"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1ad46c3ec4e12f4a4b6835e173ba21c25e484c9d02b49770bf006ce5367c036"
-dependencies = [
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-util",
- "pin-project-lite",
- "tokio",
- "tower",
- "tower-service",
-]
-
-[[package]]
-name = "backtrace"
-version = "0.3.71"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
-dependencies = [
- "addr2line",
- "cc",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
-]
-
[[package]]
name = "base-x"
version = "0.2.11"
@@ -423,7 +286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
"memchr",
- "regex-automata 0.4.6",
+ "regex-automata",
"serde",
]
@@ -448,12 +311,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
-[[package]]
-name = "bytes"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
-
[[package]]
name = "cbc"
version = "0.1.2"
@@ -705,7 +562,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
- "serde",
]
[[package]]
@@ -875,12 +731,6 @@ dependencies = [
"miniz_oxide",
]
-[[package]]
-name = "fnv"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -890,45 +740,6 @@ dependencies = [
"percent-encoding",
]
-[[package]]
-name = "futures-channel"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
-dependencies = [
- "futures-core",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
-
-[[package]]
-name = "futures-sink"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
-
-[[package]]
-name = "futures-task"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
-
-[[package]]
-name = "futures-util"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
-dependencies = [
- "futures-core",
- "futures-task",
- "pin-project-lite",
- "pin-utils",
-]
-
[[package]]
name = "fxhash"
version = "0.2.1"
@@ -970,12 +781,6 @@ dependencies = [
"polyval",
]
-[[package]]
-name = "gimli"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
-
[[package]]
name = "git-ref-format"
version = "0.3.0"
@@ -1544,40 +1349,11 @@ dependencies = [
"subtle",
]
-[[package]]
-name = "h2"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
-dependencies = [
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "futures-util",
- "http",
- "indexmap",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
-dependencies = [
- "ahash",
- "allocator-api2",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hmac"
@@ -1597,89 +1373,6 @@ dependencies = [
"windows-sys 0.52.0",
]
-[[package]]
-name = "http"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http-body"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
-dependencies = [
- "bytes",
- "http",
-]
-
-[[package]]
-name = "http-body-util"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
-dependencies = [
- "bytes",
- "futures-core",
- "http",
- "http-body",
- "pin-project-lite",
-]
-
-[[package]]
-name = "httparse"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
-
-[[package]]
-name = "httpdate"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
-
-[[package]]
-name = "hyper"
-version = "1.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "h2",
- "http",
- "http-body",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "smallvec",
- "tokio",
- "want",
-]
-
-[[package]]
-name = "hyper-util"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
-dependencies = [
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "hyper",
- "pin-project-lite",
- "socket2",
- "tokio",
-]
-
[[package]]
name = "iana-time-zone"
version = "0.1.60"
@@ -1888,30 +1581,6 @@ version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
-[[package]]
-name = "lru"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
-dependencies = [
- "hashbrown",
-]
-
-[[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
-[[package]]
-name = "matchit"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
-
[[package]]
name = "maybe-async"
version = "0.2.10"
@@ -1938,12 +1607,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "mime"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
-
[[package]]
name = "miniz_oxide"
version = "0.7.2"
@@ -1953,17 +1616,6 @@ dependencies = [
"adler",
]
-[[package]]
-name = "mio"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
-dependencies = [
- "libc",
- "wasi",
- "windows-sys 0.48.0",
-]
-
[[package]]
name = "multibase"
version = "0.9.1"
@@ -2030,16 +1682,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
-[[package]]
-name = "nu-ansi-term"
-version = "0.46.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
-dependencies = [
- "overload",
- "winapi",
-]
-
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@@ -2093,16 +1735,6 @@ dependencies = [
"libm",
]
-[[package]]
-name = "num_cpus"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
-dependencies = [
- "hermit-abi",
- "libc",
-]
-
[[package]]
name = "num_threads"
version = "0.1.7"
@@ -2118,15 +1750,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
-[[package]]
-name = "object"
-version = "0.32.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "once_cell"
version = "1.19.0"
@@ -2139,12 +1762,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
[[package]]
name = "p256"
version = "0.13.2"
@@ -2231,38 +1848,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-[[package]]
-name = "pin-project"
-version = "1.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
-dependencies = [
- "pin-project-internal",
-]
-
-[[package]]
-name = "pin-project-internal"
-version = "1.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.60",
-]
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-
[[package]]
name = "pkcs1"
version = "0.7.5"
@@ -2448,44 +2033,10 @@ dependencies = [
"pretty_assertions",
"qcheck",
"qcheck-macros",
- "radicle-cob 0.11.0",
- "radicle-crypto 0.10.0",
- "radicle-git-ext",
- "radicle-ssh 0.9.0",
- "serde",
- "serde_json",
- "siphasher 1.0.1",
- "sqlite",
- "tempfile",
- "thiserror",
- "unicode-normalization",
-]
-
-[[package]]
-name = "radicle"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2c96b3901ca5b7bfe06da3fb18105c32dc5f9f5c48a217cfc7104385a687195"
-dependencies = [
- "amplify",
- "base64 0.21.7",
- "chrono",
- "colored",
- "crossbeam-channel",
- "cyphernet",
- "fastrand",
- "git2",
- "libc",
- "localtime",
- "log",
- "multibase",
- "nonempty 0.9.0",
- "once_cell",
- "qcheck",
- "radicle-cob 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-crypto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "radicle-cob",
+ "radicle-crypto",
"radicle-git-ext",
- "radicle-ssh 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "radicle-ssh",
"serde",
"serde_json",
"siphasher 1.0.1",
@@ -2507,14 +2058,14 @@ dependencies = [
"log",
"nonempty 0.9.0",
"pretty_assertions",
- "radicle 0.11.0",
- "radicle-cli-test 0.10.0",
- "radicle-cob 0.11.0",
- "radicle-crypto 0.10.0",
+ "radicle",
+ "radicle-cli-test",
+ "radicle-cob",
+ "radicle-crypto",
"radicle-git-ext",
"radicle-node",
"radicle-surf",
- "radicle-term 0.10.0",
+ "radicle-term",
"serde",
"serde_json",
"shlex",
@@ -2539,71 +2090,13 @@ dependencies = [
]
[[package]]
-name = "radicle-cli"
+name = "radicle-cli-test"
version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5347c326bec844b7ced9c8f8bcfec88104ea2066c029d49d5128dd09a4148c50"
-dependencies = [
- "anyhow",
- "chrono",
- "git-ref-format",
- "lexopt",
- "localtime",
- "log",
- "nonempty 0.9.0",
- "radicle 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-cli-test 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-cob 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-crypto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-git-ext",
- "radicle-surf",
- "radicle-term 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde",
- "serde_json",
- "shlex",
- "tempfile",
- "thiserror",
- "timeago",
- "tree-sitter",
- "tree-sitter-bash",
- "tree-sitter-c",
- "tree-sitter-css",
- "tree-sitter-go",
- "tree-sitter-highlight",
- "tree-sitter-html",
- "tree-sitter-json",
- "tree-sitter-md",
- "tree-sitter-python",
- "tree-sitter-ruby",
- "tree-sitter-rust",
- "tree-sitter-toml",
- "tree-sitter-typescript",
- "zeroize",
-]
-
-[[package]]
-name = "radicle-cli-test"
-version = "0.10.0"
-dependencies = [
- "escargot",
- "log",
- "pretty_assertions",
- "radicle 0.11.0",
- "shlex",
- "snapbox",
- "thiserror",
-]
-
-[[package]]
-name = "radicle-cli-test"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5bbd1dc7cb2801693d6d00f937021adb0d398e9fec6b998e4830ebba32fdfdd"
dependencies = [
"escargot",
"log",
"pretty_assertions",
- "radicle 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "radicle",
"shlex",
"snapbox",
"thiserror",
@@ -2620,8 +2113,8 @@ dependencies = [
"once_cell",
"qcheck",
"qcheck-macros",
- "radicle-crypto 0.10.0",
- "radicle-dag 0.9.0",
+ "radicle-crypto",
+ "radicle-dag",
"radicle-git-ext",
"serde",
"serde_json",
@@ -2629,25 +2122,6 @@ dependencies = [
"thiserror",
]
-[[package]]
-name = "radicle-cob"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36d8268661b22cec768bdf687aa9d98db2dcd9c8f974e8208f8658244074b539"
-dependencies = [
- "fastrand",
- "git2",
- "log",
- "nonempty 0.9.0",
- "once_cell",
- "radicle-crypto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-dag 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-git-ext",
- "serde",
- "serde_json",
- "thiserror",
-]
-
[[package]]
name = "radicle-crdt"
version = "0.1.0"
@@ -2656,7 +2130,7 @@ dependencies = [
"num-traits",
"qcheck",
"qcheck-macros",
- "radicle-crypto 0.10.0",
+ "radicle-crypto",
"serde",
"tempfile",
"thiserror",
@@ -2674,7 +2148,7 @@ dependencies = [
"qcheck",
"qcheck-macros",
"radicle-git-ext",
- "radicle-ssh 0.9.0",
+ "radicle-ssh",
"serde",
"sqlite",
"ssh-key",
@@ -2683,39 +2157,9 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "radicle-crypto"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb86116dc5d9daa0d0b8e07fb71c9887d537b3fecebffc0cde6624b07176c711"
-dependencies = [
- "amplify",
- "cyphernet",
- "ec25519",
- "fastrand",
- "multibase",
- "qcheck",
- "radicle-git-ext",
- "radicle-ssh 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde",
- "sqlite",
- "ssh-key",
- "thiserror",
- "zeroize",
-]
-
-[[package]]
-name = "radicle-dag"
-version = "0.9.0"
-dependencies = [
- "fastrand",
-]
-
[[package]]
name = "radicle-dag"
version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a678c3049a88ae6a34dd9f52ea9a5f9f066a0af63466b75cf8c48840303067"
dependencies = [
"fastrand",
]
@@ -2735,7 +2179,7 @@ dependencies = [
"gix-transport 0.42.0",
"log",
"nonempty 0.9.0",
- "radicle 0.11.0",
+ "radicle",
"radicle-git-ext",
"thiserror",
]
@@ -2754,43 +2198,6 @@ dependencies = [
"thiserror",
]
-[[package]]
-name = "radicle-httpd"
-version = "0.10.0"
-dependencies = [
- "anyhow",
- "axum",
- "axum-auth",
- "axum-server",
- "base64 0.21.7",
- "chrono",
- "fastrand",
- "flate2",
- "hyper",
- "lexopt",
- "lru",
- "nonempty 0.9.0",
- "pretty_assertions",
- "radicle 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-cli 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-crypto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "radicle-surf",
- "radicle-term 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde",
- "serde_json",
- "tempfile",
- "thiserror",
- "time",
- "tokio",
- "tower",
- "tower-http",
- "tracing",
- "tracing-logfmt",
- "tracing-subscriber",
- "ureq",
- "url",
-]
-
[[package]]
name = "radicle-node"
version = "0.9.0"
@@ -2815,11 +2222,11 @@ dependencies = [
"once_cell",
"qcheck",
"qcheck-macros",
- "radicle 0.11.0",
- "radicle-crypto 0.10.0",
+ "radicle",
+ "radicle-crypto",
"radicle-fetch",
"radicle-git-ext",
- "radicle-signals 0.9.0",
+ "radicle-signals",
"scrypt",
"serde",
"serde_json",
@@ -2835,9 +2242,9 @@ name = "radicle-remote-helper"
version = "0.9.0"
dependencies = [
"log",
- "radicle 0.11.0",
- "radicle-cli 0.10.0",
- "radicle-crypto 0.10.0",
+ "radicle",
+ "radicle-cli",
+ "radicle-crypto",
"radicle-git-ext",
"thiserror",
]
@@ -2850,31 +2257,9 @@ dependencies = [
"libc",
]
-[[package]]
-name = "radicle-signals"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0633d483e40eb96a8e57264727f1c4f0d188348eb5c155cf1369469c121c6c87"
-dependencies = [
- "crossbeam-channel",
- "libc",
-]
-
-[[package]]
-name = "radicle-ssh"
-version = "0.9.0"
-dependencies = [
- "byteorder",
- "log",
- "thiserror",
- "zeroize",
-]
-
[[package]]
name = "radicle-ssh"
version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbee758010fb64482be4b18591fbeb3cbc15b16450d143edf4edb5484c7366c6"
dependencies = [
"byteorder",
"log",
@@ -2902,7 +2287,6 @@ dependencies = [
"nonempty 0.5.0",
"radicle-git-ext",
"radicle-std-ext",
- "serde",
"tar",
"thiserror",
"url",
@@ -2920,7 +2304,7 @@ dependencies = [
"libc",
"once_cell",
"pretty_assertions",
- "radicle-signals 0.9.0",
+ "radicle-signals",
"shlex",
"tempfile",
"termion 3.0.0",
@@ -2930,37 +2314,15 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "radicle-term"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a46c7b39b0fabe11cbb1f697979f1e1021122aef76b476f5d385c48a02400310"
-dependencies = [
- "anstyle-query",
- "anyhow",
- "crossbeam-channel",
- "git2",
- "inquire",
- "libc",
- "once_cell",
- "radicle-signals 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "shlex",
- "termion 3.0.0",
- "thiserror",
- "unicode-display-width",
- "unicode-segmentation",
- "zeroize",
-]
-
[[package]]
name = "radicle-tools"
version = "0.9.0"
dependencies = [
"anyhow",
- "radicle 0.11.0",
- "radicle-cli 0.10.0",
+ "radicle",
+ "radicle-cli",
"radicle-git-ext",
- "radicle-term 0.10.0",
+ "radicle-term",
]
[[package]]
@@ -3016,17 +2378,8 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.6",
- "regex-syntax 0.8.3",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -3037,15 +2390,9 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.3",
+ "regex-syntax",
]
-[[package]]
-name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
[[package]]
name = "regex-syntax"
version = "0.8.3"
@@ -3083,12 +2430,6 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "rustc-demangle"
-version = "0.1.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
-
[[package]]
name = "rustix"
version = "0.38.34"
@@ -3102,12 +2443,6 @@ dependencies = [
"windows-sys 0.52.0",
]
-[[package]]
-name = "rustversion"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
-
[[package]]
name = "ryu"
version = "1.0.17"
@@ -3195,28 +2530,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "serde_path_to_error"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
-dependencies = [
- "itoa",
- "serde",
-]
-
-[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
-]
-
[[package]]
name = "sha1_smol"
version = "1.0.0"
@@ -3244,15 +2557,6 @@ dependencies = [
"keccak",
]
-[[package]]
-name = "sharded-slab"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
-dependencies = [
- "lazy_static",
-]
-
[[package]]
name = "shell-words"
version = "1.1.0"
@@ -3299,15 +2603,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
-[[package]]
-name = "slab"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
-
[[package]]
name = "smallvec"
version = "1.13.2"
@@ -3479,18 +2774,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "sync_wrapper"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
-
-[[package]]
-name = "sync_wrapper"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
-
[[package]]
name = "tar"
version = "0.4.40"
@@ -3558,16 +2841,6 @@ dependencies = [
"syn 2.0.60",
]
-[[package]]
-name = "thread_local"
-version = "1.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
-
[[package]]
name = "time"
version = "0.3.36"
@@ -3622,154 +2895,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-[[package]]
-name = "tokio"
-version = "1.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
-dependencies = [
- "backtrace",
- "bytes",
- "libc",
- "mio",
- "num_cpus",
- "pin-project-lite",
- "socket2",
- "tokio-macros",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "tokio-macros"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.60",
-]
-
-[[package]]
-name = "tokio-util"
-version = "0.7.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
-dependencies = [
- "bytes",
- "futures-core",
- "futures-sink",
- "pin-project-lite",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "tower"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
-dependencies = [
- "futures-core",
- "futures-util",
- "pin-project",
- "pin-project-lite",
- "tokio",
- "tower-layer",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "tower-http"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
-dependencies = [
- "bitflags 2.5.0",
- "bytes",
- "http",
- "http-body",
- "http-body-util",
- "pin-project-lite",
- "tower-layer",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "tower-layer"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
-
-[[package]]
-name = "tower-service"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
-
-[[package]]
-name = "tracing"
-version = "0.1.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
-dependencies = [
- "log",
- "pin-project-lite",
- "tracing-attributes",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-attributes"
-version = "0.1.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.60",
-]
-
-[[package]]
-name = "tracing-core"
-version = "0.1.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
-dependencies = [
- "once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-logfmt"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22b8e455f6caa5212a102ec530bf86b8dc5a4c536299bffd84b238fed9119be7"
-dependencies = [
- "time",
- "tracing",
- "tracing-core",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "tracing-subscriber"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex",
- "sharded-slab",
- "thread_local",
- "tracing",
- "tracing-core",
-]
-
[[package]]
name = "tree-sitter"
version = "0.20.10"
@@ -3911,12 +3036,6 @@ dependencies = [
"tree-sitter",
]
-[[package]]
-name = "try-lock"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
-
[[package]]
name = "typenum"
version = "1.17.0"
@@ -3975,20 +3094,6 @@ dependencies = [
"subtle",
]
-[[package]]
-name = "ureq"
-version = "2.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35"
-dependencies = [
- "base64 0.21.7",
- "log",
- "once_cell",
- "serde",
- "serde_json",
- "url",
-]
-
[[package]]
name = "url"
version = "2.5.0"
@@ -3998,7 +3103,6 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
- "serde",
]
[[package]]
@@ -4007,12 +3111,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
-[[package]]
-name = "valuable"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
-
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -4035,15 +3133,6 @@ dependencies = [
"winapi-util",
]
-[[package]]
-name = "want"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
-dependencies = [
- "try-lock",
-]
-
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@@ -4104,22 +3193,6 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
[[package]]
name = "winapi-util"
version = "0.1.7"
@@ -4129,12 +3202,6 @@ dependencies = [
"windows-sys 0.52.0",
]
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -4318,26 +3385,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
-[[package]]
-name = "zerocopy"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
-dependencies = [
- "zerocopy-derive",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.60",
-]
-
[[package]]
name = "zeroize"
version = "1.7.0"
diff --git a/Cargo.toml b/Cargo.toml
index f9b60eec..42cef9ba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,6 @@ members = [
"radicle-crypto",
"radicle-dag",
"radicle-fetch",
- "radicle-httpd",
"radicle-node",
"radicle-remote-helper",
"radicle-ssh",
diff --git a/HACKING.md b/HACKING.md
index d50904db..7fc7a890 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -24,7 +24,6 @@ The repository is structured in *crates*, as follows:
* `radicle-crdt`: Conflict-free replicated datatypes (CRDTs) used for things like discussions and patches.
* `radicle-crypto`: A wrapper around Ed25519 cryptographic signing primitives.
* `radicle-dag`: A simple directed acyclic graph implementation used by `radicle-cob`.
-* `radicle-httpd`: The radicle HTTP daemon that serves API clients and Git fetch requests.
* `radicle-node`: The radicle peer-to-peer daemon that enables users to connect to the network and share code.
* `radicle-remote-helper`: A Git remote helper for `rad://` remotes.
* `radicle-ssh`: OpenSSH functionality, including a library used to interface with `ssh-agent`.
@@ -41,11 +40,6 @@ For example, the equivalent of `rad auth` in debug mode would be:
Arguments after the `--` are passed directly to the `rad` executable.
-When running the radicle node, you may specify an alternate port for the `git-daemon`
-like so:
-
- $ cargo run -p radicle-node -- --git-daemon 127.0.0.1:9876
-
This is useful if you are running multiple nodes on the same machine. You can also
specify different listen addresses for the peer-to-peer protocol using `--listen`.
To view all options, run `cargo run -p radicle-node -- --help`.
@@ -82,8 +76,8 @@ avoid storing development keys with `ssh-agent`.
## Logging
-Logging for `radicle-node` and `radicle-httpd` is turned on by default. Check
-the respective `--help` output to set the log level.
+Logging for `radicle-node` is turned on by default. Check the respective
+`--help` output to set the log level.
## Writing tests
diff --git a/README.md b/README.md
index 109ba292..a6c939db 100644
--- a/README.md
+++ b/README.md
@@ -51,9 +51,8 @@ Or directly from our seed node:
## Running
-*Systemd* unit files are provided for the node and HTTP daemon under the
-`/systemd` folder. They can be used as a starting point for further
-customization.
+*Systemd* unit files are provided for the node under the `/systemd` folder.
+They can be used as a starting point for further customization.
For running in debug mode, see [HACKING.md](HACKING.md).
diff --git a/build/Dockerfile b/build/Dockerfile
index cf01fd8f..3a7efc4d 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -45,7 +45,6 @@ RUN cargo zigbuild --locked --release \
--target=aarch64-unknown-linux-musl \
--target=x86_64-unknown-linux-musl \
-p radicle-node \
- -p radicle-httpd \
-p radicle-remote-helper \
-p radicle-cli
@@ -57,28 +56,24 @@ COPY --from=builder \
/src/target/x86_64-unknown-linux-musl/release/rad-web \
/src/target/x86_64-unknown-linux-musl/release/git-remote-rad \
/src/target/x86_64-unknown-linux-musl/release/radicle-node \
- /src/target/x86_64-unknown-linux-musl/release/radicle-httpd \
/builds/x86_64-unknown-linux-musl/bin/
COPY --from=builder \
/src/target/aarch64-unknown-linux-musl/release/rad \
/src/target/aarch64-unknown-linux-musl/release/rad-web \
/src/target/aarch64-unknown-linux-musl/release/git-remote-rad \
/src/target/aarch64-unknown-linux-musl/release/radicle-node \
- /src/target/aarch64-unknown-linux-musl/release/radicle-httpd \
/builds/aarch64-unknown-linux-musl/bin/
COPY --from=builder \
/src/target/aarch64-apple-darwin/release/rad \
/src/target/aarch64-apple-darwin/release/rad-web \
/src/target/aarch64-apple-darwin/release/git-remote-rad \
/src/target/aarch64-apple-darwin/release/radicle-node \
- /src/target/aarch64-apple-darwin/release/radicle-httpd \
/builds/aarch64-apple-darwin/bin/
COPY --from=builder \
/src/target/x86_64-apple-darwin/release/rad \
/src/target/x86_64-apple-darwin/release/rad-web \
/src/target/x86_64-apple-darwin/release/git-remote-rad \
/src/target/x86_64-apple-darwin/release/radicle-node \
- /src/target/x86_64-apple-darwin/release/radicle-httpd \
/builds/x86_64-apple-darwin/bin/
COPY --from=builder /src/*.1 /builds/x86_64-unknown-linux-musl/man/man1/
COPY --from=builder /src/*.1 /builds/aarch64-unknown-linux-musl/man/man1/
diff --git a/debian/rules b/debian/rules
index 677f4f43..8472b0f4 100755
--- a/debian/rules
+++ b/debian/rules
@@ -12,7 +12,6 @@ override_dh_auto_install:
cargo install --locked --path=radicle-cli --root=debian/radicle
cargo install --locked --path=radicle-node --root=debian/radicle
cargo install --locked --path=radicle-remote-helper --root=debian/radicle
- cargo install --locked --path=radicle-httpd --root=debian/radicle
rm -f debian/*/.crates*.*
override_dh_auto_test:
diff --git a/flake.nix b/flake.nix
index 2d06af35..3c3d15bd 100644
--- a/flake.nix
+++ b/flake.nix
@@ -170,10 +170,6 @@
crates = builtins.listToAttrs (map
({name, ...} @ package: lib.nameValuePair name (crate package))
[
- {
- name = "radicle-httpd";
- pages = ["radicle-httpd.1.adoc"];
- }
{
name = "radicle-cli";
pages = [
@@ -233,11 +229,6 @@
drv = self.packages.${system}.radicle-node;
};
- apps.radicle-httpd = flake-utils.lib.mkApp {
- name = "radicle-httpd";
- drv = self.packages.${system}.radicle-httpd;
- };
-
devShells.default = craneLib.devShell {
# Extra inputs can be added here; cargo and rustc are provided by default.
packages = with pkgs; [
diff --git a/radicle-httpd.1.adoc b/radicle-httpd.1.adoc
deleted file mode 100644
index 3be21bc3..00000000
--- a/radicle-httpd.1.adoc
+++ /dev/null
@@ -1,25 +0,0 @@
-= radicle-httpd(1)
-The Radicle Team <team@radicle.xyz>
-:doctype: manpage
-:revnumber: 1.0.0
-:revdate: 2024-04-22
-:mansource: rad {revnumber}
-:manmanual: Radicle CLI Manual
-
-== Name
-
-radicle-httpd - Radicle HTTP daemon
-
-== Synopsis
-
-*radicle-httpd* --help
-
-== Description
-
-A Radicle HTTP daemon exposing a JSON HTTP API that allows someone to browse local
-repositories on a Radicle node via their web browser. This manual page is a
-placeholder to point you at the *--help* option.
-
-== SEE ALSO ==
-
-*rad*(1)
diff --git a/radicle-httpd/Cargo.toml b/radicle-httpd/Cargo.toml
deleted file mode 100644
index e242fa76..00000000
--- a/radicle-httpd/Cargo.toml
+++ /dev/null
@@ -1,67 +0,0 @@
-[package]
-name = "radicle-httpd"
-description = "Radicle HTTP daemon"
-homepage = "https://radicle.xyz"
-license = "MIT OR Apache-2.0"
-version = "0.10.0"
-authors = ["cloudhead <cloudhead@radicle.xyz>"]
-edition = "2021"
-default-run = "radicle-httpd"
-build = "build.rs"
-
-[features]
-default = []
-logfmt = [
- "tracing-logfmt",
- "tracing-subscriber/env-filter"
-]
-
-[[bin]]
-name = "radicle-httpd"
-path = "src/main.rs"
-
-[[bin]]
-name = "rad-web"
-path = "src/bin/rad-web.rs"
-
-[dependencies]
-anyhow = { version = "1" }
-axum = { version = "0.7.2", default-features = false, features = ["json", "query", "tokio", "http1"] }
-axum-auth = { version= "0.7.0", default-features = false, features = ["auth-bearer"] }
-axum-server = { version = "0.6.0", default-features = false }
-base64 = "0.21.3"
-chrono = { version = "0.4.22", default-features = false, features = ["clock"] }
-fastrand = { version = "2.0.0" }
-flate2 = { version = "1" }
-hyper = { version = "1.0.1", default-features = false }
-lexopt = { version = "0.3.0" }
-lru = { version = "0.12.0" }
-nonempty = { version = "0.9.0", features = ["serialize"] }
-radicle-surf = { version = "0.21.0", default-features = false, features = ["serde"] }
-serde = { version = "1", features = ["derive"] }
-serde_json = { version = "1", features = ["preserve_order"] }
-thiserror = { version = "1" }
-time = { version = "0.3.17", features = ["parsing", "serde"] }
-tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] }
-tower-http = { version = "0.5", default-features = false, features = ["trace", "cors", "set-header"] }
-tracing = { version = "0.1.37", default-features = false, features = ["std", "log"] }
-tracing-logfmt = { version = "0.3", optional = true }
-tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "ansi", "fmt"] }
-ureq = { version = "2.9", default-features = false, features = ["json"] }
-url = { version = "2.5.0" }
-
-[dependencies.radicle]
-version = "0.11.0"
-
-[dependencies.radicle-term]
-version = "0.10.0"
-
-[dependencies.radicle-cli]
-version = "0.10.0"
-
-[dev-dependencies]
-hyper = { version = "1.0.1", default-features = false, features = ["client"] }
-pretty_assertions = { version = "1.3.0" }
-radicle-crypto = { version = "0.10.0", features = ["test"] }
-tempfile = { version = "3.3.0" }
-tower = { version = "0.4", features = ["util"] }
diff --git a/radicle-httpd/build.rs b/radicle-httpd/build.rs
deleted file mode 120000
index 10238032..00000000
--- a/radicle-httpd/build.rs
+++ /dev/null
@@ -1 +0,0 @@
-../build.rs
\ No newline at end of file
diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs
deleted file mode 100644
index 36bb531c..00000000
--- a/radicle-httpd/src/api.rs
+++ /dev/null
@@ -1,261 +0,0 @@
-pub mod auth;
-
-use std::collections::HashMap;
-use std::sync::Arc;
-use std::time::Duration;
-
-use axum::http::header::{AUTHORIZATION, CONTENT_TYPE};
-use axum::http::Method;
-use axum::response::{IntoResponse, Json};
-use axum::routing::get;
-use axum::Router;
-use radicle::issue::cache::Issues as _;
-use radicle::patch::cache::Patches as _;
-use radicle::storage::git::Repository;
-use serde::{Deserialize, Serialize};
-use serde_json::json;
-use tokio::sync::RwLock;
-use tower_http::cors::{self, CorsLayer};
-
-use radicle::cob::{issue, patch, Author};
-use radicle::identity::{DocAt, RepoId};
-use radicle::node::policy::Scope;
-use radicle::node::routing::Store;
-use radicle::node::AliasStore;
-use radicle::node::{Handle, NodeId};
-use radicle::storage::{ReadRepository, ReadStorage};
-use radicle::{Node, Profile};
-
-mod error;
-mod json;
-mod v1;
-
-use crate::api::error::Error;
-use crate::cache::Cache;
-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 = "0.1.0";
-
-/// Identifier for sessions
-type SessionId = String;
-
-#[derive(Clone)]
-pub struct Context {
- profile: Arc<Profile>,
- sessions: Arc<RwLock<HashMap<SessionId, auth::Session>>>,
- cache: Option<Cache>,
-}
-
-impl Context {
- pub fn new(profile: Arc<Profile>, options: &Options) -> Self {
- Self {
- profile,
- sessions: Default::default(),
- cache: options.cache.map(Cache::new),
- }
- }
-
- pub fn project_info<R: ReadRepository + radicle::cob::Store>(
- &self,
- repo: &R,
- doc: DocAt,
- ) -> Result<project::Info, error::Error> {
- let (_, head) = repo.head()?;
- let DocAt { doc, .. } = doc;
- let id = repo.id();
-
- let payload = doc.project()?;
- let aliases = self.profile.aliases();
- let delegates = doc
- .delegates
- .into_iter()
- .map(|did| json::author(&Author::new(did), aliases.alias(did.as_key())))
- .collect::<Vec<_>>();
- let issues = self.profile.issues(repo)?.counts()?;
- let patches = self.profile.patches(repo)?.counts()?;
- let db = &self.profile.database()?;
- let seeding = db.count(&id).unwrap_or_default();
-
- Ok(project::Info {
- payload,
- delegates,
- threshold: doc.threshold,
- visibility: doc.visibility,
- head,
- issues,
- patches,
- id,
- seeding,
- })
- }
-
- /// Get a repository by RID, checking to make sure we're allowed to view it.
- pub fn repo(&self, rid: RepoId) -> Result<(Repository, DocAt), error::Error> {
- let repo = self.profile.storage.repository(rid)?;
- let doc = repo.identity_doc()?;
- // Don't allow accessing private repos.
- if doc.visibility.is_private() {
- return Err(Error::NotFound);
- }
- Ok((repo, doc))
- }
-
- #[cfg(test)]
- pub fn profile(&self) -> &Arc<Profile> {
- &self.profile
- }
-
- #[cfg(test)]
- pub fn sessions(&self) -> &Arc<RwLock<HashMap<SessionId, auth::Session>>> {
- &self.sessions
- }
-}
-
-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,
- Method::POST,
- Method::PATCH,
- Method::PUT,
- Method::DELETE,
- ])
- .allow_headers([CONTENT_TYPE, AUTHORIZATION]),
- )
-}
-
-async fn root_handler() -> impl IntoResponse {
- let response = json!({
- "path": "/api",
- "links": [
- {
- "href": "/v1",
- "rel": "v1",
- "type": "GET"
- }
- ]
- });
-
- Json(response)
-}
-
-#[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 {
- pub mime: Option<String>,
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct CobsQuery<T> {
- pub page: Option<usize>,
- pub per_page: Option<usize>,
- pub state: Option<T>,
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct PoliciesQuery {
- /// The NID from which to fetch from after tracking a repo.
- pub from: Option<NodeId>,
- pub scope: Option<Scope>,
-}
-
-#[derive(Default, Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub enum IssueState {
- Closed,
- #[default]
- Open,
-}
-
-impl IssueState {
- pub fn matches(&self, issue: &issue::State) -> bool {
- match self {
- Self::Open => matches!(issue, issue::State::Open),
- Self::Closed => matches!(issue, issue::State::Closed { .. }),
- }
- }
-}
-
-#[derive(Default, Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub enum PatchState {
- #[default]
- Open,
- Draft,
- Archived,
- Merged,
-}
-
-impl PatchState {
- pub fn matches(&self, patch: &patch::State) -> bool {
- match self {
- Self::Open => matches!(patch, patch::State::Open { .. }),
- Self::Draft => matches!(patch, patch::State::Draft),
- Self::Archived => matches!(patch, patch::State::Archived),
- Self::Merged => matches!(patch, patch::State::Merged { .. }),
- }
- }
-}
-
-mod project {
- use serde::Serialize;
- use serde_json::Value;
-
- use radicle::cob;
- use radicle::git::Oid;
- use radicle::identity::project::Project;
- use radicle::identity::{RepoId, Visibility};
-
- /// Project info.
- #[derive(Serialize)]
- #[serde(rename_all = "camelCase")]
- pub struct Info {
- /// Project metadata.
- #[serde(flatten)]
- pub payload: Project,
- pub delegates: Vec<Value>,
- pub threshold: usize,
- pub visibility: Visibility,
- pub head: Oid,
- pub patches: cob::patch::PatchCounts,
- pub issues: cob::issue::IssueCounts,
- pub id: RepoId,
- pub seeding: usize,
- }
-}
-
-/// Announce refs to the network for the given RID.
-pub fn announce_refs(mut node: Node, rid: RepoId) -> Result<(), Error> {
- match node.announce_refs(rid) {
- Ok(_) => Ok(()),
- Err(e) if e.is_connection_err() => Ok(()),
- Err(e) => Err(e.into()),
- }
-}
diff --git a/radicle-httpd/src/api/auth.rs b/radicle-httpd/src/api/auth.rs
deleted file mode 100644
index 255de775..00000000
--- a/radicle-httpd/src/api/auth.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use serde::{Deserialize, Serialize};
-use time::serde::timestamp;
-use time::{Duration, OffsetDateTime};
-
-use radicle::crypto::PublicKey;
-use radicle::node::Alias;
-
-use crate::api::error::Error;
-use crate::api::Context;
-
-pub const UNAUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::seconds(60);
-pub const AUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::weeks(1);
-
-#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
-#[serde(rename_all = "lowercase")]
-pub enum AuthState {
- Authorized,
- Unauthorized,
-}
-
-#[derive(Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Session {
- pub status: AuthState,
- pub public_key: PublicKey,
- pub alias: Alias,
- #[serde(with = "timestamp")]
- pub issued_at: OffsetDateTime,
- #[serde(with = "timestamp")]
- pub expires_at: OffsetDateTime,
-}
-
-pub async fn validate(ctx: &Context, token: &str) -> Result<(), Error> {
- let sessions_store = ctx.sessions.read().await;
- let session = sessions_store
- .get(token)
- .ok_or(Error::Auth("Unauthorized"))?;
-
- if session.status != AuthState::Authorized || session.expires_at <= OffsetDateTime::now_utc() {
- return Err(Error::Auth("Unauthorized"));
- }
-
- Ok(())
-}
diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs
deleted file mode 100644
index da100566..00000000
--- a/radicle-httpd/src/api/error.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-use axum::http::StatusCode;
-use axum::response::{IntoResponse, Response};
-use axum::Json;
-use serde_json::json;
-
-/// Errors relating to the API backend.
-#[derive(Debug, thiserror::Error)]
-pub enum Error {
- /// The entity was not found.
- #[error("entity not found")]
- NotFound,
-
- /// An error occurred during an authentication process.
- #[error("could not authenticate: {0}")]
- Auth(&'static str),
-
- /// An error occurred with env variables.
- #[error(transparent)]
- Env(#[from] std::env::VarError),
-
- /// Profile error.
- #[error(transparent)]
- Profile(#[from] radicle::profile::Error),
-
- /// Crypto error.
- #[error(transparent)]
- Crypto(#[from] radicle::crypto::Error),
-
- /// Storage error.
- #[error(transparent)]
- Storage(#[from] radicle::storage::Error),
-
- /// Cob cache error.
- #[error(transparent)]
- CobCache(#[from] radicle::cob::cache::Error),
-
- /// Cob issue cache error.
- #[error(transparent)]
- CacheIssue(#[from] radicle::cob::issue::cache::Error),
-
- /// Cob issue error.
- #[error(transparent)]
- CobIssue(#[from] radicle::cob::issue::Error),
-
- /// Cob patch error.
- #[error(transparent)]
- CobPatch(#[from] radicle::cob::patch::Error),
-
- /// Cob patch cache error.
- #[error(transparent)]
- CachePatch(#[from] radicle::cob::patch::cache::Error),
-
- /// Cob store error.
- #[error(transparent)]
- CobStore(#[from] radicle::cob::store::Error),
-
- /// Repository error.
- #[error(transparent)]
- Repository(#[from] radicle::storage::RepositoryError),
-
- /// Routing error.
- #[error(transparent)]
- Routing(#[from] radicle::node::routing::Error),
-
- /// Project doc error.
- #[error(transparent)]
- ProjectDoc(#[from] radicle::identity::doc::PayloadError),
-
- /// Surf directory error.
- #[error(transparent)]
- SurfDir(#[from] radicle_surf::fs::error::Directory),
-
- /// Surf error.
- #[error(transparent)]
- Surf(#[from] radicle_surf::Error),
-
- /// Git2 error.
- #[error(transparent)]
- Git2(#[from] radicle::git::raw::Error),
-
- /// Storage refs error.
- #[error(transparent)]
- StorageRef(#[from] radicle::storage::refs::Error),
-
- /// Identity doc error.
- #[error(transparent)]
- IdentityDoc(#[from] radicle::identity::doc::DocError),
-
- /// Tracking store error.
- #[error(transparent)]
- TrackingStore(#[from] radicle::node::policy::store::Error),
-
- /// Node database error.
- #[error(transparent)]
- Database(#[from] radicle::node::db::Error),
-
- /// Node error.
- #[error(transparent)]
- Node(#[from] radicle::node::Error),
-
- /// Invalid update to issue or patch.
- #[error("{0}")]
- BadRequest(String),
-}
-
-impl IntoResponse for Error {
- fn into_response(self) -> Response {
- let message = self.to_string();
- let (status, msg) = match self {
- Error::NotFound => (StatusCode::NOT_FOUND, None),
- Error::CobStore(e @ radicle::cob::store::Error::NotFound(_, _)) => {
- (StatusCode::NOT_FOUND, Some(e.to_string()))
- }
- Error::Auth(msg) => (StatusCode::UNAUTHORIZED, Some(msg.to_string())),
- Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
- Error::Surf(radicle_surf::Error::Git(e)) if radicle::git::is_not_found_err(&e) => {
- (StatusCode::NOT_FOUND, Some(e.message().to_owned()))
- }
- Error::Surf(radicle_surf::Error::Directory(
- e @ radicle_surf::fs::error::Directory::PathNotFound(_),
- )) => (StatusCode::NOT_FOUND, Some(e.to_string())),
- Error::Git2(e) if radicle::git::is_not_found_err(&e) => {
- (StatusCode::NOT_FOUND, Some(e.message().to_owned()))
- }
- Error::Git2(e) => (
- StatusCode::INTERNAL_SERVER_ERROR,
- Some(e.message().to_owned()),
- ),
- Error::Storage(err) if err.is_not_found() => {
- (StatusCode::NOT_FOUND, Some(err.to_string()))
- }
- Error::Repository(err) if err.is_not_found() => {
- (StatusCode::NOT_FOUND, Some(err.to_string()))
- }
- Error::StorageRef(err) if err.is_not_found() => {
- (StatusCode::NOT_FOUND, Some(err.to_string()))
- }
- Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, Some(msg)),
- other => {
- tracing::error!("Error: {message}");
- tracing::debug!("Error Debug: {:?}", other);
-
- if cfg!(debug_assertions) {
- (StatusCode::INTERNAL_SERVER_ERROR, Some(other.to_string()))
- } else {
- (StatusCode::INTERNAL_SERVER_ERROR, None)
- }
- }
- };
-
- let body = Json(json!({
- "error": msg.or_else(|| status.canonical_reason().map(|r| r.to_string())),
- "code": status.as_u16()
- }));
-
- (status, body).into_response()
- }
-}
diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs
deleted file mode 100644
index 2686d306..00000000
--- a/radicle-httpd/src/api/json.rs
+++ /dev/null
@@ -1,312 +0,0 @@
-//! Utilities for building JSON responses of our API.
-
-use std::collections::BTreeMap;
-use std::path::Path;
-use std::str;
-
-use base64::prelude::{Engine, BASE64_STANDARD};
-use radicle::cob::{CodeLocation, Reaction};
-use radicle::patch::ReviewId;
-use serde_json::{json, Value};
-
-use radicle::cob::issue::{Issue, IssueId};
-use radicle::cob::patch::{Merge, Patch, PatchId, Review};
-use radicle::cob::thread::{Comment, CommentId, Edit};
-use radicle::cob::{ActorId, Author};
-use radicle::git::RefString;
-use radicle::node::{Alias, AliasStore};
-use radicle::prelude::NodeId;
-use radicle::storage::{git, refs, RemoteRepository};
-use radicle_surf::blob::Blob;
-use radicle_surf::tree::{EntryKind, Tree};
-use radicle_surf::{Commit, Oid};
-
-use crate::api::auth::Session;
-
-/// Returns JSON of a commit.
-pub(crate) fn commit(commit: &Commit) -> Value {
- json!({
- "id": commit.id,
- "author": {
- "name": commit.author.name,
- "email": commit.author.email
- },
- "summary": commit.summary,
- "description": commit.description(),
- "parents": commit.parents,
- "committer": {
- "name": commit.committer.name,
- "email": commit.committer.email,
- "time": commit.committer.time.seconds()
- }
- })
-}
-
-/// Returns JSON of a session.
-pub(crate) fn session(session_id: String, session: &Session) -> Value {
- json!({
- "sessionId": session_id,
- "status": session.status,
- "publicKey": session.public_key,
- "alias": session.alias,
- "issuedAt": session.issued_at.unix_timestamp(),
- "expiresAt": session.expires_at.unix_timestamp()
- })
-}
-
-/// Returns JSON for a blob with a given `path`.
-pub(crate) fn blob<T: AsRef<[u8]>>(blob: &Blob<T>, path: &str) -> Value {
- json!({
- "binary": blob.is_binary(),
- "name": name_in_path(path),
- "content": blob_content(blob),
- "path": path,
- "lastCommit": commit(blob.commit())
- })
-}
-
-/// Returns a string for the blob content, encoded in base64 if binary.
-pub fn blob_content<T: AsRef<[u8]>>(blob: &Blob<T>) -> String {
- match str::from_utf8(blob.content()) {
- Ok(s) => s.to_owned(),
- Err(_) => BASE64_STANDARD.encode(blob.content()),
- }
-}
-
-/// Returns JSON for a tree with a given `path` and `stats`.
-pub(crate) fn tree(tree: &Tree, path: &str) -> Value {
- let prefix = Path::new(path);
- let entries = tree
- .entries()
- .iter()
- .map(|entry| {
- json!({
- "path": prefix.join(entry.name()),
- "oid": entry.object_id(),
- "name": entry.name(),
- "kind": match entry.entry() {
- EntryKind::Tree(_) => "tree",
- EntryKind::Blob(_) => "blob",
- EntryKind::Submodule { .. } => "submodule"
- },
- })
- })
- .collect::<Vec<_>>();
-
- json!({
- "entries": &entries,
- "lastCommit": commit(tree.commit()),
- "name": name_in_path(path),
- "path": path,
- })
-}
-
-/// Returns JSON for an `issue`.
-pub(crate) fn issue(id: IssueId, issue: Issue, aliases: &impl AliasStore) -> Value {
- json!({
- "id": id.to_string(),
- "author": author(&issue.author(), aliases.alias(issue.author().id())),
- "title": issue.title(),
- "state": issue.state(),
- "assignees": issue.assignees().map(|assignee|
- author(&Author::from(*assignee.as_key()), aliases.alias(assignee))
- ).collect::<Vec<_>>(),
- "discussion": issue.comments().map(|(id, c)| issue_comment(id, c, aliases)).collect::<Vec<_>>(),
- "labels": issue.labels().collect::<Vec<_>>(),
- })
-}
-
-/// Returns JSON for a `patch`.
-pub(crate) fn patch(
- id: PatchId,
- patch: Patch,
- repo: &git::Repository,
- aliases: &impl AliasStore,
-) -> Value {
- json!({
- "id": id.to_string(),
- "author": author(patch.author(), aliases.alias(patch.author().id())),
- "title": patch.title(),
- "state": patch.state(),
- "target": patch.target(),
- "labels": patch.labels().collect::<Vec<_>>(),
- "merges": patch.merges().map(|(nid, m)| merge(nid, m, aliases)).collect::<Vec<_>>(),
- "assignees": patch.assignees().map(|assignee|
- author(&Author::from(*assignee), aliases.alias(&assignee))
- ).collect::<Vec<_>>(),
- "revisions": patch.revisions().map(|(id, rev)| {
- json!({
- "id": id,
- "author": author(rev.author(), aliases.alias(rev.author().id())),
- "description": rev.description(),
- "edits": rev.edits().map(|e| edit(e, aliases)).collect::<Vec<_>>(),
- "reactions": rev.reactions().iter().flat_map(|(location, reaction)| {
- reactions(reaction.iter().fold(BTreeMap::new(), |mut acc: BTreeMap<&Reaction, Vec<_>>, (author, emoji)| {
- acc.entry(emoji).or_default().push(author);
- acc
- }), location.as_ref(), aliases)
- }).collect::<Vec<_>>(),
- "base": rev.base(),
- "oid": rev.head(),
- "refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or_default(),
- "discussions": rev.discussion().comments().map(|(id, c)| {
- patch_comment(id, c, aliases)
- }).collect::<Vec<_>>(),
- "timestamp": rev.timestamp().as_secs(),
- "reviews": patch.reviews_of(id).map(move |(id, r)| {
- review(id, r, aliases)
- }).collect::<Vec<_>>(),
- })
- }).collect::<Vec<_>>(),
- })
-}
-
-/// Returns JSON for a `reaction`.
-fn reactions(
- reactions: BTreeMap<&Reaction, Vec<&ActorId>>,
- location: Option<&CodeLocation>,
- aliases: &impl AliasStore,
-) -> Vec<Value> {
- reactions
- .into_iter()
- .map(|(emoji, authors)| {
- if let Some(l) = location {
- json!({ "location": l, "emoji": emoji, "authors": authors.into_iter().map(|a|
- author(&Author::from(*a), aliases.alias(a))
- ).collect::<Vec<_>>()})
- } else {
- json!({ "emoji": emoji, "authors": authors.into_iter().map(|a|
- author(&Author::from(*a), aliases.alias(a))
- ).collect::<Vec<_>>()})
- }
- })
- .collect::<Vec<_>>()
-}
-
-/// Returns JSON for an `author` and fills in `alias` when present.
-pub(crate) fn author(author: &Author, alias: Option<Alias>) -> Value {
- match alias {
- Some(alias) => json!({
- "id": author.id,
- "alias": alias,
- }),
- None => json!(author),
- }
-}
-
-/// Returns JSON for a patch `Merge` and fills in `alias` when present.
-fn merge(nid: &NodeId, merge: &Merge, aliases: &impl AliasStore) -> Value {
- json!({
- "author": author(&Author::from(*nid), aliases.alias(nid)),
- "commit": merge.commit,
- "timestamp": merge.timestamp.as_secs(),
- "revision": merge.revision,
- })
-}
-
-/// Returns JSON for a patch `Review` and fills in `alias` when present.
-fn review(id: &ReviewId, review: &Review, aliases: &impl AliasStore) -> Value {
- let a = review.author();
- json!({
- "id": id,
- "author": author(a, aliases.alias(a.id())),
- "verdict": review.verdict(),
- "summary": review.summary(),
- "comments": review.comments().map(|(id, c)| review_comment(id, c, aliases)).collect::<Vec<_>>(),
- "timestamp": review.timestamp().as_secs(),
- })
-}
-
-/// Returns JSON for an `Edit`.
-fn edit(edit: &Edit, aliases: &impl AliasStore) -> Value {
- json!({
- "author": author(&Author::from(edit.author), aliases.alias(&edit.author)),
- "body": edit.body,
- "timestamp": edit.timestamp.as_secs(),
- "embeds": edit.embeds,
- })
-}
-
-/// Returns JSON for a Issue `Comment`.
-fn issue_comment(id: &CommentId, comment: &Comment, aliases: &impl AliasStore) -> Value {
- json!({
- "id": *id,
- "author": author(&Author::from(comment.author()), aliases.alias(&comment.author())),
- "body": comment.body(),
- "edits": comment.edits().map(|e| edit(e, aliases)).collect::<Vec<_>>(),
- "embeds": comment.embeds().to_vec(),
- "reactions": reactions(comment.reactions(), None, aliases),
- "timestamp": comment.timestamp().as_secs(),
- "replyTo": comment.reply_to(),
- "resolved": comment.is_resolved(),
- })
-}
-
-/// Returns JSON for a Patch `Comment`.
-fn patch_comment(
- id: &CommentId,
- comment: &Comment<CodeLocation>,
- aliases: &impl AliasStore,
-) -> Value {
- json!({
- "id": *id,
- "author": author(&Author::from(comment.author()), aliases.alias(&comment.author())),
- "body": comment.body(),
- "edits": comment.edits().map(|e| edit(e, aliases)).collect::<Vec<_>>(),
- "embeds": comment.embeds().to_vec(),
- "reactions": reactions(comment.reactions(), None, aliases),
- "timestamp": comment.timestamp().as_secs(),
- "replyTo": comment.reply_to(),
- "location": comment.location(),
- "resolved": comment.is_resolved(),
- })
-}
-
-/// Returns JSON for a `Review`.
-fn review_comment(
- id: &CommentId,
- comment: &Comment<CodeLocation>,
- aliases: &impl AliasStore,
-) -> Value {
- json!({
- "id": *id,
- "author": author(&Author::from(comment.author()), aliases.alias(&comment.author())),
- "body": comment.body(),
- "edits": comment.edits().map(|e| edit(e, aliases)).collect::<Vec<_>>(),
- "embeds": comment.embeds().to_vec(),
- "reactions": reactions(comment.reactions(), None, aliases),
- "timestamp": comment.timestamp().as_secs(),
- "replyTo": comment.reply_to(),
- "location": comment.location(),
- "resolved": comment.is_resolved(),
- })
-}
-
-/// Returns the name part of a path string.
-fn name_in_path(path: &str) -> &str {
- match path.rsplit('/').next() {
- Some(name) => name,
- None => path,
- }
-}
-
-fn get_refs(
- repo: &git::Repository,
- id: &ActorId,
- head: &Oid,
-) -> Result<Vec<RefString>, refs::Error> {
- let remote = repo.remote(id)?;
- let refs = remote
- .refs
- .iter()
- .filter_map(|(name, o)| {
- if o == head {
- Some(name.to_owned())
- } else {
- None
- }
- })
- .collect::<Vec<_>>();
-
- Ok(refs)
-}
diff --git a/radicle-httpd/src/api/v1.rs b/radicle-httpd/src/api/v1.rs
deleted file mode 100644
index 3815091b..00000000
--- a/radicle-httpd/src/api/v1.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-mod delegates;
-mod node;
-mod profile;
-mod projects;
-mod sessions;
-mod stats;
-
-use axum::extract::State;
-use axum::response::{IntoResponse, Json};
-use axum::routing::get;
-use axum::Router;
-use serde_json::json;
-
-use crate::api::{Context, API_VERSION, RADICLE_VERSION};
-
-pub fn router(ctx: Context) -> Router {
- let root_router = Router::new()
- .route("/", get(root_handler))
- .with_state(ctx.clone());
-
- let routes = Router::new()
- .merge(root_router)
- .merge(node::router(ctx.clone()))
- .merge(profile::router(ctx.clone()))
- .merge(sessions::router(ctx.clone()))
- .merge(delegates::router(ctx.clone()))
- .merge(projects::router(ctx.clone()))
- .merge(stats::router(ctx));
-
- Router::new().nest("/v1", routes)
-}
-
-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")),
- "apiVersion": API_VERSION,
- "nid": ctx.profile.public_key,
- "path": "/api/v1",
- "links": [
- {
- "href": "/projects",
- "rel": "projects",
- "type": "GET"
- },
- {
- "href": "/node",
- "rel": "node",
- "type": "GET"
- },
- {
- "href": "/delegates/:did/projects",
- "rel": "projects",
- "type": "GET"
- },
- {
- "href": "/profile",
- "rel": "profile",
- "type": "GET"
- },
- {
- "href": "/stats",
- "rel": "stats",
- "type": "GET"
- }
- ]
- });
-
- Json(response)
-}
diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs
deleted file mode 100644
index bfd53bb5..00000000
--- a/radicle-httpd/src/api/v1/delegates.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-use axum::extract::State;
-use axum::response::IntoResponse;
-use axum::routing::get;
-use axum::{Json, Router};
-
-use radicle::cob::Author;
-use radicle::identity::Did;
-use radicle::issue::cache::Issues as _;
-use radicle::node::routing::Store;
-use radicle::node::AliasStore;
-use radicle::patch::cache::Patches as _;
-use radicle::storage::{ReadRepository, ReadStorage};
-
-use crate::api::error::Error;
-use crate::api::json;
-use crate::api::project::Info;
-use crate::api::Context;
-use crate::api::{PaginationQuery, ProjectQuery};
-use crate::axum_extra::{Path, Query};
-
-pub fn router(ctx: Context) -> Router {
- Router::new()
- .route(
- "/delegates/:delegate/projects",
- get(delegates_projects_handler),
- )
- .with_state(ctx)
-}
-
-/// List all projects which delegate is a part of.
-/// `GET /delegates/:delegate/projects`
-async fn delegates_projects_handler(
- State(ctx): State<Context>,
- Path(delegate): Path<Did>,
- Query(qs): Query<PaginationQuery>,
-) -> impl IntoResponse {
- 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 pinned = &ctx.profile.config.web.pinned;
- let mut projects = match show {
- ProjectQuery::All => storage
- .repositories()?
- .into_iter()
- .filter(|repo| repo.doc.visibility.is_public())
- .collect::<Vec<_>>(),
- ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?,
- };
- projects.sort_by_key(|p| p.rid);
-
- let infos = projects
- .into_iter()
- .filter_map(|id| {
- if !id.doc.delegates.iter().any(|d| *d == delegate) {
- return None;
- }
- let Ok(repo) = storage.repository(id.rid) else {
- return None;
- };
- let Ok((_, head)) = repo.head() else {
- return None;
- };
- let Ok(payload) = id.doc.project() else {
- return None;
- };
- let Ok(issues) = ctx.profile.issues(&repo) else {
- return None;
- };
- let Ok(issues) = issues.counts() else {
- return None;
- };
- let Ok(patches) = ctx.profile.patches(&repo) else {
- return None;
- };
- let Ok(patches) = patches.counts() else {
- return None;
- };
-
- let aliases = ctx.profile.aliases();
- let delegates = id
- .doc
- .delegates
- .into_iter()
- .map(|did| json::author(&Author::new(did), aliases.alias(did.as_key())))
- .collect::<Vec<_>>();
- let seeding = db.count(&id.rid).unwrap_or_default();
-
- Some(Info {
- payload,
- delegates,
- threshold: id.doc.threshold,
- visibility: id.doc.visibility,
- head,
- issues,
- patches,
- id: id.rid,
- seeding,
- })
- })
- .skip(page * per_page)
- .take(per_page)
- .collect::<Vec<_>>();
-
- Ok::<_, Error>(Json(infos))
-}
-
-#[cfg(test)]
-mod routes {
- use std::net::SocketAddr;
-
- use axum::extract::connect_info::MockConnectInfo;
- use axum::http::StatusCode;
- use serde_json::json;
-
- use crate::test::{self, get, HEAD, RID};
-
- #[tokio::test]
- async fn test_delegates_projects() {
- let tmp = tempfile::tempdir().unwrap();
- let seed = test::seed(tmp.path());
- let app = super::router(seed.clone())
- .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))));
- let response = get(
- &app,
- "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all",
- )
- .await;
-
- assert_eq!(
- response.status(),
- StatusCode::OK,
- "failed response: {:?}",
- response.json().await
- );
- assert_eq!(
- response.json().await,
- json!([
- {
- "name": "hello-world",
- "description": "Rad repository for tests",
- "defaultBranch": "master",
- "delegates": [
- {
- "id": "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
- "alias": "seed"
- }
- ],
- "threshold": 1,
- "visibility": {
- "type": "public"
- },
- "head": HEAD,
- "patches": {
- "open": 1,
- "draft": 0,
- "archived": 0,
- "merged": 0,
- },
- "issues": {
- "open": 1,
- "closed": 0,
- },
- "id": RID,
- "seeding": 0,
- },
- ])
- );
-
- let app = super::router(seed).layer(MockConnectInfo(SocketAddr::from((
- [192, 168, 13, 37],
- 8080,
- ))));
- let response = get(
- &app,
- "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all",
- )
- .await;
-
- assert_eq!(
- response.status(),
- StatusCode::OK,
- "failed response: {:?}",
- response.json().await
- );
- assert_eq!(
- response.json().await,
- json!([
- {
- "name": "hello-world",
- "description": "Rad repository for tests",
- "defaultBranch": "master",
- "delegates": [
- {
- "id": "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
- "alias": "seed"
- }
- ],
- "threshold": 1,
- "visibility": {
- "type": "public"
- },
- "head": HEAD,
- "patches": {
- "open": 1,
- "draft": 0,
- "archived": 0,
- "merged": 0,
- },
- "issues": {
- "open": 1,
- "closed": 0,
- },
- "id": RID,
- "seeding": 0,
- }
- ])
- );
- }
-}
diff --git a/radicle-httpd/src/api/v1/node.rs b/radicle-httpd/src/api/v1/node.rs
deleted file mode 100644
index 3e238238..00000000
--- a/radicle-httpd/src/api/v1/node.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-use axum::extract::State;
-use axum::response::IntoResponse;
-use axum::routing::{get, put};
-use axum::{Json, Router};
-use axum_auth::AuthBearer;
-use hyper::StatusCode;
-use serde_json::json;
-
-use radicle::identity::RepoId;
-use radicle::node::routing::Store;
-use radicle::node::{
- policy::{Policy, SeedPolicy},
- AliasStore, Handle, NodeId, DEFAULT_TIMEOUT,
-};
-use radicle::Node;
-
-use crate::api::error::Error;
-use crate::api::{self, Context, PoliciesQuery, RADICLE_VERSION};
-use crate::axum_extra::{Path, Query};
-
-pub fn router(ctx: Context) -> Router {
- Router::new()
- .route("/node", get(node_handler))
- .route("/node/policies/repos", get(node_policies_repos_handler))
- .route(
- "/node/policies/repos/:rid",
- put(node_policies_seed_handler).delete(node_policies_unseed_handler),
- )
- .route("/nodes/:nid", get(nodes_handler))
- .route("/nodes/:nid/inventory", get(nodes_inventory_handler))
- .with_state(ctx)
-}
-
-/// Return local node information.
-/// `GET /node`
-async fn node_handler(State(ctx): State<Context>) -> impl IntoResponse {
- let node = Node::new(ctx.profile.socket());
- let node_id = ctx.profile.public_key;
- let node_state = if node.is_running() {
- "running"
- } else {
- "stopped"
- };
- let config = match node.config() {
- Ok(config) => Some(config),
- Err(err) => {
- tracing::error!("Error getting node config: {:#}", err);
- None
- }
- };
- let response = json!({
- "id": node_id.to_string(),
- "version": format!("{}-{}", RADICLE_VERSION, env!("GIT_HEAD")),
- "config": config,
- "state": node_state,
- });
-
- Ok::<_, Error>(Json(response))
-}
-
-/// Return stored information about other nodes.
-/// `GET /nodes/:nid`
-async fn nodes_handler(State(ctx): State<Context>, Path(nid): Path<NodeId>) -> impl IntoResponse {
- let aliases = ctx.profile.aliases();
- let response = json!({
- "alias": aliases.alias(&nid),
- });
-
- Ok::<_, Error>(Json(response))
-}
-
-/// Return stored information about other nodes.
-/// `GET /nodes/:nid/inventory`
-async fn nodes_inventory_handler(
- State(ctx): State<Context>,
- Path(nid): Path<NodeId>,
-) -> impl IntoResponse {
- let db = &ctx.profile.database()?;
- let resources = db.get_resources(&nid)?;
-
- Ok::<_, Error>(Json(resources))
-}
-
-/// Return local repo policies information.
-/// `GET /node/policies/repos`
-async fn node_policies_repos_handler(State(ctx): State<Context>) -> impl IntoResponse {
- let policies = ctx.profile.policies()?;
- let mut repos = Vec::new();
-
- for SeedPolicy { rid: id, policy } in policies.seed_policies()? {
- repos.push(json!({
- "id": id,
- "scope": policy.scope().unwrap_or_default(),
- "policy": Policy::from(policy),
- }));
- }
-
- Ok::<_, Error>(Json(repos))
-}
-
-/// Seed a new repo.
-/// `PUT /node/policies/repos/:rid`
-async fn node_policies_seed_handler(
- State(ctx): State<Context>,
- AuthBearer(token): AuthBearer,
- Path(project): Path<RepoId>,
- Query(qs): Query<PoliciesQuery>,
-) -> impl IntoResponse {
- api::auth::validate(&ctx, &token).await?;
- let mut node = Node::new(ctx.profile.socket());
- node.seed(project, qs.scope.unwrap_or_default())?;
-
- if let Some(from) = qs.from {
- let results = node.fetch(project, from, DEFAULT_TIMEOUT)?;
- return Ok::<_, Error>((
- StatusCode::OK,
- Json(json!({ "success": true, "results": results })),
- ));
- }
- Ok::<_, Error>((StatusCode::OK, Json(json!({ "success": true }))))
-}
-
-/// Unseed a repo.
-/// `DELETE /node/policies/repos/:rid`
-async fn node_policies_unseed_handler(
- State(ctx): State<Context>,
- AuthBearer(token): AuthBearer,
- Path(project): Path<RepoId>,
-) -> impl IntoResponse {
- api::auth::validate(&ctx, &token).await?;
- let mut node = Node::new(ctx.profile.socket());
- node.unseed(project)?;
-
- Ok::<_, Error>((StatusCode::OK, Json(json!({ "success": true }))))
-}
diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs
deleted file mode 100644
index 6b869af1..00000000
--- a/radicle-httpd/src/api/v1/profile.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-use std::net::SocketAddr;
-
-use axum::extract::{ConnectInfo, State};
-use axum::response::IntoResponse;
-use axum::routing::get;
-use axum::{Json, Router};
-use serde_json::json;
-
-use crate::api::error::Error;
-use crate::api::Context;
-
-pub fn router(ctx: Context) -> Router {
- Router::new()
- .route("/profile", get(profile_handler))
- .with_state(ctx)
-}
-
-/// Return local profile information.
-/// `GET /profile`
-async fn profile_handler(
- State(ctx): State<Context>,
- ConnectInfo(addr): ConnectInfo<SocketAddr>,
-) -> impl IntoResponse {
- if !addr.ip().is_loopback() {
- return Err(Error::Auth("Profile data is only shown for localhost"));
- }
-
- Ok::<_, Error>(Json(
- json!({ "config": ctx.profile.config, "home": ctx.profile.home.path() }),
- ))
-}
-
-#[cfg(test)]
-mod routes {
- use std::net::SocketAddr;
-
- use axum::extract::connect_info::MockConnectInfo;
- use axum::http::StatusCode;
- use serde_json::json;
-
- use crate::test::{self, get};
-
- #[tokio::test]
- async fn test_remote_profile() {
- let tmp = tempfile::tempdir().unwrap();
- let seed = test::seed(tmp.path());
- let app = super::router(seed.clone())
- .layer(MockConnectInfo(SocketAddr::from(([192, 168, 1, 1], 8080))));
- let response = get(&app, "/profile").await;
-
- assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
- assert_eq!(
- response.json().await,
- json!({
- "error": "Profile data is only shown for localhost",
- "code": 401
- })
- )
- }
-
- #[tokio::test]
- async fn test_profile() {
- let tmp = tempfile::tempdir().unwrap();
- let seed = test::seed(tmp.path());
- let app = super::router(seed.clone())
- .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))));
- let response = get(&app, "/profile").await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "config": {
- "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
- "preferredSeeds": [
- "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776",
- "z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo@ash.radicle.garden:8776"
- ],
- "web": { "pinned": { "repositories": [] } },
- "cli": {
- "hints": true
- },
- "node": {
- "alias": "seed",
- "listen": [],
- "peers": { "type": "dynamic" },
- "connect": [],
- "externalAddresses": [],
- "network": "main",
- "log": "INFO",
- "relay": "auto",
- "limits": {
- "routingMaxSize": 1000,
- "routingMaxAge": 604800,
- "gossipMaxAge": 1209600,
- "fetchConcurrency": 1,
- "maxOpenFiles": 4096,
- "rate": {
- "inbound": {
- "fillRate": 5.0,
- "capacity": 1024
- },
- "outbound": {
- "fillRate": 10.0,
- "capacity": 2048
- }
- },
- "connection": {
- "inbound": 128,
- "outbound": 16
- }
- },
- "workers": 8,
- "seedingPolicy": {
- "default": "block",
- }
- }
- },
- "home": seed.profile.path()
- })
- );
- }
-}
diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs
deleted file mode 100644
index 9efa7052..00000000
--- a/radicle-httpd/src/api/v1/projects.rs
+++ /dev/null
@@ -1,3565 +0,0 @@
-use std::collections::{BTreeMap, HashMap};
-
-use axum::extract::{DefaultBodyLimit, State};
-use axum::handler::Handler;
-use axum::http::{header, HeaderValue};
-use axum::response::{IntoResponse, Response};
-use axum::routing::{get, patch, post};
-use axum::{Json, Router};
-use axum_auth::AuthBearer;
-use hyper::StatusCode;
-use radicle_surf::blob::BlobRef;
-use serde::{Deserialize, Serialize};
-use serde_json::json;
-use tower_http::set_header::SetResponseHeaderLayer;
-
-use radicle::cob::{
- issue, issue::cache::Issues as _, patch, patch::cache::Patches as _, resolve_embed, Author,
- Embed, Label, Uri,
-};
-use radicle::identity::{Did, RepoId};
-use radicle::node::routing::Store;
-use radicle::node::{AliasStore, Node, NodeId};
-use radicle::storage::{ReadRepository, ReadStorage, RemoteRepository, WriteRepository};
-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, ProjectQuery};
-use crate::axum_extra::{immutable_response, Path, Query};
-
-const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";
-const MAX_BODY_LIMIT: usize = 4_194_304;
-
-pub fn router(ctx: Context) -> Router {
- Router::new()
- .route("/projects", get(project_root_handler))
- .route("/projects/:project", get(project_handler))
- .route("/projects/:project/commits", get(history_handler))
- .route("/projects/:project/commits/:sha", get(commit_handler))
- .route("/projects/:project/diff/:base/:oid", get(diff_handler))
- .route(
- "/projects/:project/activity",
- get(
- activity_handler.layer(SetResponseHeaderLayer::if_not_present(
- header::CACHE_CONTROL,
- |response: &Response| {
- response
- .status()
- .is_success()
- .then_some(HeaderValue::from_static(CACHE_1_HOUR))
- },
- )),
- ),
- )
- .route("/projects/:project/tree/:sha/", get(tree_handler_root))
- .route("/projects/:project/tree/:sha/*path", get(tree_handler))
- .route(
- "/projects/:project/stats/tree/:sha",
- get(stats_tree_handler),
- )
- .route("/projects/:project/remotes", get(remotes_handler))
- .route("/projects/:project/remotes/:peer", get(remote_handler))
- .route("/projects/:project/blob/:sha/*path", get(blob_handler))
- .route("/projects/:project/readme/:sha", get(readme_handler))
- .route(
- "/projects/:project/issues",
- post(issue_create_handler).get(issues_handler),
- )
- .route(
- "/projects/:project/issues/:id",
- patch(issue_update_handler).get(issue_handler),
- )
- .route(
- "/projects/:project/patches",
- post(patch_create_handler).get(patches_handler),
- )
- .route(
- "/projects/:project/patches/:id",
- patch(patch_update_handler).get(patch_handler),
- )
- .with_state(ctx)
- .layer(DefaultBodyLimit::max(MAX_BODY_LIMIT))
-}
-
-/// List all projects.
-/// `GET /projects`
-async fn project_root_handler(
- State(ctx): State<Context>,
- Query(qs): Query<PaginationQuery>,
-) -> impl IntoResponse {
- 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 pinned = &ctx.profile.config.web.pinned;
- let policies = ctx.profile.policies()?;
-
- let mut projects = match show {
- ProjectQuery::All => storage
- .repositories()?
- .into_iter()
- .filter(|repo| repo.doc.visibility.is_public())
- .collect::<Vec<_>>(),
- ProjectQuery::Pinned => storage
- .repositories_by_id(pinned.repositories.iter())?
- .into_iter()
- .filter(|repo| repo.doc.visibility.is_public())
- .collect::<Vec<_>>(),
- };
- projects.sort_by_key(|p| p.rid);
-
- let infos = projects
- .into_iter()
- .filter_map(|info| {
- if !policies.is_seeding(&info.rid).unwrap_or_default() {
- return None;
- }
- let Ok(repo) = storage.repository(info.rid) else {
- return None;
- };
- let Ok((_, head)) = repo.head() else {
- return None;
- };
- let Ok(payload) = info.doc.project() else {
- return None;
- };
- let Ok(issues) = ctx.profile.issues(&repo) else {
- return None;
- };
- let Ok(issues) = issues.counts() else {
- return None;
- };
- let Ok(patches) = ctx.profile.patches(&repo) else {
- return None;
- };
- let Ok(patches) = patches.counts() else {
- return None;
- };
- let aliases = ctx.profile.aliases();
- let delegates = info
- .doc
- .delegates
- .into_iter()
- .map(|did| api::json::author(&Author::new(did), aliases.alias(did.as_key())))
- .collect::<Vec<_>>();
- let seeding = db.count(&info.rid).unwrap_or_default();
-
- Some(Info {
- payload,
- delegates,
- head,
- threshold: info.doc.threshold,
- visibility: info.doc.visibility,
- issues,
- patches,
- id: info.rid,
- seeding,
- })
- })
- .skip(page * per_page)
- .take(per_page)
- .collect::<Vec<_>>();
-
- Ok::<_, Error>(Json(infos))
-}
-
-/// Get project metadata.
-/// `GET /projects/:project`
-async fn project_handler(State(ctx): State<Context>, Path(rid): Path<RepoId>) -> impl IntoResponse {
- let (repo, doc) = ctx.repo(rid)?;
- let info = ctx.project_info(&repo, doc)?;
-
- Ok::<_, Error>(Json(info))
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct CommitsQueryString {
- pub parent: Option<String>,
- pub since: Option<i64>,
- pub until: Option<i64>,
- pub page: Option<usize>,
- pub per_page: Option<usize>,
-}
-
-/// Get project commit range.
-/// `GET /projects/:project/commits?parent=<sha>`
-async fn history_handler(
- State(ctx): State<Context>,
- Path(rid): Path<RepoId>,
- Query(qs): Query<CommitsQueryString>,
-) -> impl IntoResponse {
- let (repo, doc) = ctx.repo(rid)?;
- let CommitsQueryString {
- since,
- until,
- parent,
- page,
- per_page,
- } = qs;
-
- // If the parent commit is provided, the response depends only on the query
- // string and not on the state of the repository. This means we can instruct
- // the caches to treat the response as immutable.
- let is_immutable = parent.is_some();
-
- let sha = match parent {
- Some(commit) => commit,
- None => ctx.project_info(&repo, doc)?.head.to_string(),
- };
- let repo = Repository::open(repo.path())?;
-
- // If a pagination is defined, we do not want to paginate the commits, and we return all of them on the first page.
- let page = page.unwrap_or(0);
- let per_page = if per_page.is_none() && (since.is_some() || until.is_some()) {
- usize::MAX
- } else {
- per_page.unwrap_or(30)
- };
-
- let commits = repo
- .history(&sha)?
- .filter_map(|commit| {
- let commit = commit.ok()?;
- let time = commit.committer.time.seconds();
- let commit = api::json::commit(&commit);
- match (since, until) {
- (Some(since), Some(until)) if time >= since && time < until => Some(commit),
- (Some(since), None) if time >= since => Some(commit),
- (None, Some(until)) if time < until => Some(commit),
- (None, None) => Some(commit),
- _ => None,
- }
- })
- .skip(page * per_page)
- .take(per_page)
- .collect::<Vec<_>>();
-
- if is_immutable {
- Ok::<_, Error>(immutable_response(commits).into_response())
- } else {
- Ok::<_, Error>(Json(commits).into_response())
- }
-}
-
-/// Get project commit.
-/// `GET /projects/:project/commits/:sha`
-async fn commit_handler(
- State(ctx): State<Context>,
- Path((project, sha)): Path<(RepoId, Oid)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let repo = Repository::open(repo.path())?;
- let commit = repo.commit(sha)?;
-
- let diff = repo.diff_commit(commit.id)?;
- let glob = Glob::all_heads().branches().and(Glob::all_remotes());
- let branches: Vec<String> = repo
- .revision_branches(commit.id, glob)?
- .iter()
- .map(|b| b.refname().to_string())
- .collect();
-
- let mut files: HashMap<Oid, BlobRef<'_>> = HashMap::new();
- diff.files().for_each(|file_diff| match file_diff {
- diff::FileDiff::Added(added) => {
- if let Ok(blob) = repo.blob_ref(added.new.oid) {
- files.insert(blob.id(), blob);
- }
- }
- diff::FileDiff::Deleted(deleted) => {
- if let Ok(old_blob) = repo.blob_ref(deleted.old.oid) {
- files.insert(old_blob.id(), old_blob);
- }
- }
- diff::FileDiff::Modified(modified) => {
- if let (Ok(old_blob), Ok(new_blob)) = (
- repo.blob_ref(modified.old.oid),
- repo.blob_ref(modified.new.oid),
- ) {
- files.insert(old_blob.id(), old_blob);
- files.insert(new_blob.id(), new_blob);
- }
- }
- diff::FileDiff::Moved(moved) => {
- if let (Ok(old_blob), Ok(new_blob)) =
- (repo.blob_ref(moved.old.oid), repo.blob_ref(moved.new.oid))
- {
- files.insert(old_blob.id(), old_blob);
- files.insert(new_blob.id(), new_blob);
- }
- }
- diff::FileDiff::Copied(copied) => {
- if let (Ok(old_blob), Ok(new_blob)) =
- (repo.blob_ref(copied.old.oid), repo.blob_ref(copied.new.oid))
- {
- files.insert(old_blob.id(), old_blob);
- files.insert(new_blob.id(), new_blob);
- }
- }
- });
-
- let response: serde_json::Value = json!({
- "commit": api::json::commit(&commit),
- "diff": diff,
- "files": files,
- "branches": branches
- });
- Ok::<_, Error>(immutable_response(response))
-}
-
-/// Get diff between two commits
-/// `GET /projects/:project/diff/:base/:oid`
-async fn diff_handler(
- State(ctx): State<Context>,
- Path((project, base, oid)): Path<(RepoId, Oid, Oid)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let repo = Repository::open(repo.path())?;
- let base = repo.commit(base)?;
- let commit = repo.commit(oid)?;
- let diff = repo.diff(base.id, commit.id)?;
- let mut files: HashMap<Oid, BlobRef<'_>> = HashMap::new();
- diff.files().for_each(|file_diff| match file_diff {
- diff::FileDiff::Added(added) => {
- if let Ok(new_blob) = repo.blob_ref(added.new.oid) {
- files.insert(new_blob.id(), new_blob);
- }
- }
- diff::FileDiff::Deleted(deleted) => {
- if let Ok(old_blob) = repo.blob_ref(deleted.old.oid) {
- files.insert(old_blob.id(), old_blob);
- }
- }
- diff::FileDiff::Modified(modified) => {
- if let (Ok(new_blob), Ok(old_blob)) = (
- repo.blob_ref(modified.old.oid),
- repo.blob_ref(modified.new.oid),
- ) {
- files.insert(new_blob.id(), new_blob);
- files.insert(old_blob.id(), old_blob);
- }
- }
- diff::FileDiff::Moved(moved) => {
- if let (Ok(new_blob), Ok(old_blob)) =
- (repo.blob_ref(moved.new.oid), repo.blob_ref(moved.old.oid))
- {
- files.insert(new_blob.id(), new_blob);
- files.insert(old_blob.id(), old_blob);
- }
- }
- diff::FileDiff::Copied(copied) => {
- if let (Ok(new_blob), Ok(old_blob)) =
- (repo.blob_ref(copied.new.oid), repo.blob_ref(copied.old.oid))
- {
- files.insert(new_blob.id(), new_blob);
- files.insert(old_blob.id(), old_blob);
- }
- }
- });
-
- let commits = repo
- .history(commit.id)?
- .take_while(|c| {
- if let Ok(c) = c {
- c.id != base.id
- } else {
- false
- }
- })
- .map(|r| r.map(|c| api::json::commit(&c)))
- .collect::<Result<Vec<_>, _>>()?;
-
- let response = json!({ "diff": diff, "files": files, "commits": commits });
-
- Ok::<_, Error>(immutable_response(response))
-}
-
-/// Get project activity for the past year.
-/// `GET /projects/:project/activity`
-async fn activity_handler(
- State(ctx): State<Context>,
- Path(project): Path<RepoId>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let current_date = chrono::Utc::now().timestamp();
- // SAFETY: The number of weeks is static and not out of bounds.
- #[allow(clippy::unwrap_used)]
- let one_year_ago = chrono::Duration::try_weeks(52).unwrap();
- let repo = Repository::open(repo.path())?;
- let head = repo.head()?;
- let timestamps = repo
- .history(head)?
- .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 }))))
-}
-
-/// Get project source tree for '/' path.
-/// `GET /projects/:project/tree/:sha/`
-async fn tree_handler_root(
- State(ctx): State<Context>,
- Path((rid, sha)): Path<(RepoId, Oid)>,
-) -> impl IntoResponse {
- tree_handler(State(ctx), Path((rid, sha, String::new()))).await
-}
-
-/// Get project source tree.
-/// `GET /projects/:project/tree/:sha/*path`
-async fn tree_handler(
- State(ctx): State<Context>,
- Path((project, sha, path)): Path<(RepoId, Oid, String)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
-
- if let Some(ref cache) = ctx.cache {
- let cache = &mut cache.tree.lock().await;
- if let Some(response) = cache.get(&(project, sha, path.clone())) {
- return Ok::<_, Error>(immutable_response(response.clone()));
- }
- }
-
- let repo = Repository::open(repo.path())?;
- let tree = repo.tree(sha, &path)?;
- let response = api::json::tree(&tree, &path);
-
- if let Some(cache) = &ctx.cache {
- let cache = &mut cache.tree.lock().await;
- cache.put((project, sha, path.clone()), response.clone());
- }
-
- Ok::<_, Error>(immutable_response(response))
-}
-
-/// Get project source tree stats.
-/// `GET /projects/:project/stats/tree/:sha`
-async fn stats_tree_handler(
- State(ctx): State<Context>,
- Path((project, sha)): Path<(RepoId, Oid)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let repo = Repository::open(repo.path())?;
- let stats = repo.stats_from(&sha)?;
-
- Ok::<_, Error>(immutable_response(stats))
-}
-
-/// Get all project remotes.
-/// `GET /projects/:project/remotes`
-async fn remotes_handler(
- State(ctx): State<Context>,
- Path(project): Path<RepoId>,
-) -> impl IntoResponse {
- let (repo, doc) = ctx.repo(project)?;
- let delegates = &doc.delegates;
- let aliases = &ctx.profile.aliases();
- let remotes = repo
- .remotes()?
- .filter_map(|r| r.map(|r| r.1).ok())
- .map(|remote| {
- let refs = remote
- .refs
- .iter()
- .filter_map(|(r, oid)| {
- r.as_str()
- .strip_prefix("refs/heads/")
- .map(|head| (head.to_string(), oid))
- })
- .collect::<BTreeMap<String, &Oid>>();
-
- match aliases.alias(&remote.id) {
- Some(alias) => json!({
- "id": remote.id,
- "alias": alias,
- "heads": refs,
- "delegate": delegates.contains(&remote.id.into()),
- }),
- None => json!({
- "id": remote.id,
- "heads": refs,
- "delegate": delegates.contains(&remote.id.into()),
- }),
- }
- })
- .collect::<Vec<_>>();
-
- Ok::<_, Error>(Json(remotes))
-}
-
-/// Get project remote.
-/// `GET /projects/:project/remotes/:peer`
-async fn remote_handler(
- State(ctx): State<Context>,
- Path((project, node_id)): Path<(RepoId, NodeId)>,
-) -> impl IntoResponse {
- let (repo, doc) = ctx.repo(project)?;
- let delegates = &doc.delegates;
- let remote = repo.remote(&node_id)?;
- let refs = remote
- .refs
- .iter()
- .filter_map(|(r, oid)| {
- r.as_str()
- .strip_prefix("refs/heads/")
- .map(|head| (head.to_string(), oid))
- })
- .collect::<BTreeMap<String, &Oid>>();
- let remote = json!({
- "id": remote.id,
- "heads": refs,
- "delegate": delegates.contains(&remote.id.into()),
- });
-
- Ok::<_, Error>(Json(remote))
-}
-
-/// Get project source file.
-/// `GET /projects/:project/blob/:sha/*path`
-async fn blob_handler(
- State(ctx): State<Context>,
- Path((project, sha, path)): Path<(RepoId, Oid, String)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let repo = Repository::open(repo.path())?;
- let blob = repo.blob(sha, &path)?;
-
- if blob.size() > MAX_BODY_LIMIT {
- return Ok::<_, Error>(
- (
- StatusCode::PAYLOAD_TOO_LARGE,
- [(header::CACHE_CONTROL, "no-cache")],
- Json(json!([])),
- )
- .into_response(),
- );
- }
- Ok::<_, Error>(immutable_response(api::json::blob(&blob, &path)).into_response())
-}
-
-/// Get project readme.
-/// `GET /projects/:project/readme/:sha`
-async fn readme_handler(
- State(ctx): State<Context>,
- Path((project, sha)): Path<(RepoId, Oid)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let repo = Repository::open(repo.path())?;
- let paths = [
- "README",
- "README.md",
- "README.markdown",
- "README.txt",
- "README.rst",
- "Readme.md",
- ];
-
- for path in paths
- .iter()
- .map(ToString::to_string)
- .chain(paths.iter().map(|p| p.to_lowercase()))
- {
- if let Ok(blob) = repo.blob(sha, &path) {
- if blob.size() > MAX_BODY_LIMIT {
- return Ok::<_, Error>(
- (
- StatusCode::PAYLOAD_TOO_LARGE,
- [(header::CACHE_CONTROL, "no-cache")],
- Json(json!([])),
- )
- .into_response(),
- );
- }
-
- return Ok::<_, Error>(
- immutable_response(api::json::blob(&blob, &path)).into_response(),
- );
- }
- }
-
- Err(Error::NotFound)
-}
-
-/// Get project issues list.
-/// `GET /projects/:project/issues`
-async fn issues_handler(
- State(ctx): State<Context>,
- Path(project): Path<RepoId>,
- Query(qs): Query<CobsQuery<api::IssueState>>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let CobsQuery {
- page,
- per_page,
- state,
- } = qs;
- let page = page.unwrap_or(0);
- let per_page = per_page.unwrap_or(10);
- let state = state.unwrap_or_default();
- let issues = ctx.profile.issues(&repo)?;
- let mut issues: Vec<_> = issues
- .list()?
- .filter_map(|r| {
- let (id, issue) = r.ok()?;
- (state.matches(issue.state())).then_some((id, issue))
- })
- .collect::<Vec<_>>();
-
- issues.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
- let aliases = &ctx.profile.aliases();
- let issues = issues
- .into_iter()
- .map(|(id, issue)| api::json::issue(id, issue, aliases))
- .skip(page * per_page)
- .take(per_page)
- .collect::<Vec<_>>();
-
- Ok::<_, Error>(Json(issues))
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-pub struct IssueCreate {
- pub title: String,
- pub description: String,
- pub labels: Vec<Label>,
- pub assignees: Vec<Did>,
- pub embeds: Vec<Embed<Uri>>,
-}
-
-/// Create a new issue.
-/// `POST /projects/:project/issues`
-async fn issue_create_handler(
- State(ctx): State<Context>,
- AuthBearer(token): AuthBearer,
- Path(project): Path<RepoId>,
- Json(issue): Json<IssueCreate>,
-) -> impl IntoResponse {
- api::auth::validate(&ctx, &token).await?;
-
- let (repo, _) = ctx.repo(project)?;
- let node = Node::new(ctx.profile.socket());
- let signer = ctx
- .profile
- .signer()
- .map_err(|_| Error::Auth("Unauthorized"))?;
- let embeds: Vec<Embed> = issue
- .embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
-
- let mut issues = ctx.profile.issues_mut(&repo)?;
- let issue = issues
- .create(
- issue.title,
- issue.description,
- &issue.labels,
- &issue.assignees,
- embeds,
- &signer,
- )
- .map_err(Error::from)?;
-
- announce_refs(node, repo.id())?;
-
- Ok::<_, Error>((
- StatusCode::CREATED,
- Json(json!({ "success": true, "id": issue.id().to_string() })),
- ))
-}
-
-/// Update an issue.
-/// `PATCH /projects/:project/issues/:id`
-async fn issue_update_handler(
- State(ctx): State<Context>,
- AuthBearer(token): AuthBearer,
- Path((project, issue_id)): Path<(RepoId, Oid)>,
- Json(action): Json<issue::Action>,
-) -> impl IntoResponse {
- api::auth::validate(&ctx, &token).await?;
-
- let (repo, _) = ctx.repo(project)?;
- let node = Node::new(ctx.profile.socket());
- let signer = ctx.profile.signer()?;
- let mut issues = ctx.profile.issues_mut(&repo)?;
- let mut issue = issues.get_mut(&issue_id.into())?;
-
- let id = match action {
- issue::Action::Assign { assignees } => issue.assign(assignees, &signer)?,
- issue::Action::Lifecycle { state } => issue.lifecycle(state, &signer)?,
- issue::Action::Label { labels } => issue.label(labels, &signer)?,
- issue::Action::Edit { title } => issue.edit(title, &signer)?,
- issue::Action::Comment {
- body,
- reply_to,
- embeds,
- } => {
- let embeds: Vec<Embed> = embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
- if let Some(to) = reply_to {
- issue.comment(body, to, embeds, &signer)?
- } else {
- return Err(Error::BadRequest("`replyTo` missing".to_owned()));
- }
- }
- issue::Action::CommentReact {
- id,
- reaction,
- active,
- } => issue.react(id, reaction, active, &signer)?,
- issue::Action::CommentEdit { id, body, embeds } => {
- let embeds: Vec<Embed> = embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
- issue.edit_comment(id, body, embeds, &signer)?
- }
- issue::Action::CommentRedact { id } => issue.redact_comment(id, &signer)?,
- };
-
- announce_refs(node, repo.id())?;
-
- Ok::<_, Error>(Json(json!({ "success": true, "id": id })))
-}
-
-/// Get project issue.
-/// `GET /projects/:project/issues/:id`
-async fn issue_handler(
- State(ctx): State<Context>,
- Path((project, issue_id)): Path<(RepoId, Oid)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(project)?;
- let issue = ctx
- .profile
- .issues(&repo)?
- .get(&issue_id.into())?
- .ok_or(Error::NotFound)?;
- let aliases = ctx.profile.aliases();
-
- Ok::<_, Error>(Json(api::json::issue(issue_id.into(), issue, &aliases)))
-}
-
-#[derive(Deserialize, Serialize)]
-pub struct PatchCreate {
- pub title: String,
- pub description: String,
- pub target: Oid,
- pub oid: Oid,
- pub labels: Vec<Label>,
-}
-
-/// Create a new patch.
-/// `POST /projects/:project/patches`
-async fn patch_create_handler(
- State(ctx): State<Context>,
- AuthBearer(token): AuthBearer,
- Path(project): Path<RepoId>,
- Json(patch): Json<PatchCreate>,
-) -> impl IntoResponse {
- api::auth::validate(&ctx, &token).await?;
-
- let node = Node::new(ctx.profile.socket());
- let signer = ctx
- .profile
- .signer()
- .map_err(|_| Error::Auth("Unauthorized"))?;
- let (repo, _) = ctx.repo(project)?;
- let mut patches = ctx.profile.patches_mut(&repo)?;
- let base_oid = repo.raw().merge_base(*patch.target, *patch.oid)?;
-
- let patch = patches
- .create(
- patch.title,
- patch.description,
- patch::MergeTarget::default(),
- base_oid,
- patch.oid,
- &patch.labels,
- &signer,
- )
- .map_err(Error::from)?;
-
- announce_refs(node, repo.id())?;
-
- Ok::<_, Error>((
- StatusCode::CREATED,
- Json(json!({ "success": true, "id": patch.id.to_string() })),
- ))
-}
-
-/// Update an patch.
-/// `PATCH /projects/:project/patches/:id`
-async fn patch_update_handler(
- State(ctx): State<Context>,
- AuthBearer(token): AuthBearer,
- Path((project, patch_id)): Path<(RepoId, Oid)>,
- Json(action): Json<patch::Action>,
-) -> impl IntoResponse {
- api::auth::validate(&ctx, &token).await?;
-
- let node = Node::new(ctx.profile.socket());
- let signer = ctx
- .profile
- .signer()
- .map_err(|_| Error::Auth("Unauthorized"))?;
- let (repo, _) = ctx.repo(project)?;
- let mut patches = ctx.profile.patches_mut(&repo)?;
- let mut patch = patches.get_mut(&patch_id.into())?;
- let id = match action {
- patch::Action::Edit { title, target } => patch.edit(title, target, &signer)?,
- patch::Action::Label { labels } => patch.label(labels, &signer)?,
- patch::Action::Lifecycle { state } => patch.lifecycle(state, &signer)?,
- patch::Action::Assign { assignees } => patch.assign(assignees, &signer)?,
- patch::Action::Merge { revision, commit } => {
- // TODO: We should cleanup the stored copy at least.
- patch.merge(revision, commit, &signer)?.entry
- }
- patch::Action::Review {
- revision,
- summary,
- verdict,
- labels,
- } => *patch.review(revision, verdict, summary, labels, &signer)?,
- patch::Action::ReviewEdit {
- review,
- summary,
- verdict,
- } => patch.edit_review(review, summary, verdict, &signer)?,
- patch::Action::ReviewRedact { review } => patch.redact_review(review, &signer)?,
- patch::Action::ReviewComment {
- review,
- body,
- reply_to,
- location,
- embeds,
- } => {
- let embeds: Vec<Embed> = embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
- patch.review_comment(review, body, location, reply_to, embeds, &signer)?
- }
- patch::Action::ReviewCommentEdit {
- review,
- comment,
- body,
- embeds,
- } => {
- let embeds: Vec<Embed> = embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
- patch.edit_review_comment(review, comment, body, embeds, &signer)?
- }
- patch::Action::ReviewCommentReact {
- review,
- comment,
- reaction,
- active,
- } => patch.react_review_comment(review, comment, reaction, active, &signer)?,
- patch::Action::ReviewCommentRedact { review, comment } => {
- patch.redact_review_comment(review, comment, &signer)?
- }
- patch::Action::ReviewCommentResolve { review, comment } => {
- patch.resolve_review_comment(review, comment, &signer)?
- }
- patch::Action::ReviewCommentUnresolve { review, comment } => {
- patch.unresolve_review_comment(review, comment, &signer)?
- }
- patch::Action::Revision {
- description,
- base,
- oid,
- ..
- } => patch.update(description, base, oid, &signer)?.into(),
- patch::Action::RevisionEdit {
- revision,
- description,
- embeds,
- } => {
- let embeds: Vec<Embed> = embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
- patch.edit_revision(revision, description, embeds, &signer)?
- }
- patch::Action::RevisionRedact { revision } => patch.redact(revision, &signer)?,
- patch::Action::RevisionReact {
- revision,
- reaction,
- active,
- location,
- } => patch.react(revision, reaction, location, active, &signer)?,
- patch::Action::RevisionComment {
- revision,
- body,
- reply_to,
- location,
- embeds,
- } => {
- let embeds: Vec<Embed> = embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
- patch.comment(revision, body, reply_to, location, embeds, &signer)?
- }
- patch::Action::RevisionCommentEdit {
- revision,
- comment,
- body,
- embeds,
- } => {
- let embeds: Vec<Embed> = embeds
- .into_iter()
- .filter_map(|embed| resolve_embed(&repo, embed))
- .collect();
- patch.comment_edit(revision, comment, body, embeds, &signer)?
- }
- patch::Action::RevisionCommentReact {
- revision,
- comment,
- reaction,
- active,
- } => patch.comment_react(revision, comment, reaction, active, &signer)?,
- patch::Action::RevisionCommentRedact { revision, comment } => {
- patch.comment_redact(revision, comment, &signer)?
- }
- };
-
- announce_refs(node, repo.id())?;
-
- Ok::<_, Error>(Json(json!({ "success": true, "id": id })))
-}
-
-/// Get project patches list.
-/// `GET /projects/:project/patches`
-async fn patches_handler(
- State(ctx): State<Context>,
- Path(rid): Path<RepoId>,
- Query(qs): Query<CobsQuery<api::PatchState>>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(rid)?;
- let CobsQuery {
- page,
- per_page,
- state,
- } = qs;
- let page = page.unwrap_or(0);
- let per_page = per_page.unwrap_or(10);
- let state = state.unwrap_or_default();
- let patches = ctx.profile.patches(&repo)?;
- let mut patches = patches
- .list()?
- .filter_map(|r| {
- let (id, patch) = r.ok()?;
- (state.matches(patch.state())).then_some((id, patch))
- })
- .collect::<Vec<_>>();
- patches.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
- let aliases = ctx.profile.aliases();
- let patches = patches
- .into_iter()
- .map(|(id, patch)| api::json::patch(id, patch, &repo, &aliases))
- .skip(page * per_page)
- .take(per_page)
- .collect::<Vec<_>>();
-
- Ok::<_, Error>(Json(patches))
-}
-
-/// Get project patch.
-/// `GET /projects/:project/patches/:id`
-async fn patch_handler(
- State(ctx): State<Context>,
- Path((rid, patch_id)): Path<(RepoId, Oid)>,
-) -> impl IntoResponse {
- let (repo, _) = ctx.repo(rid)?;
- let patches = ctx.profile.patches(&repo)?;
- let patch = patches.get(&patch_id.into())?.ok_or(Error::NotFound)?;
- let aliases = ctx.profile.aliases();
-
- Ok::<_, Error>(Json(api::json::patch(
- patch_id.into(),
- patch,
- &repo,
- &aliases,
- )))
-}
-
-#[cfg(test)]
-mod routes {
- use std::net::SocketAddr;
-
- use axum::body::Body;
- use axum::extract::connect_info::MockConnectInfo;
- use axum::http::StatusCode;
- use pretty_assertions::assert_eq;
- use radicle::storage::ReadStorage;
- use serde_json::json;
-
- use crate::test::*;
-
- #[tokio::test]
- async fn test_projects_root() {
- let tmp = tempfile::tempdir().unwrap();
- 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?show=all").await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!([
- {
- "name": "hello-world",
- "description": "Rad repository for tests",
- "defaultBranch": "master",
- "delegates": [
- {
- "id": DID,
- "alias": "seed"
- }
- ],
- "threshold": 1,
- "visibility": {
- "type": "public"
- },
- "head": HEAD,
- "patches": {
- "open": 1,
- "draft": 0,
- "archived": 0,
- "merged": 0,
- },
- "issues": {
- "open": 1,
- "closed": 0,
- },
- "id": RID,
- "seeding": 0,
- },
- ])
- );
-
- let app = super::router(seed).layer(MockConnectInfo(SocketAddr::from((
- [192, 168, 13, 37],
- 8080,
- ))));
- let response = get(&app, "/projects?show=all").await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!([
- {
- "name": "hello-world",
- "description": "Rad repository for tests",
- "defaultBranch": "master",
- "delegates": [
- {
- "id": DID,
- "alias": "seed"
- }
- ],
- "threshold": 1,
- "visibility": {
- "type": "public"
- },
- "head": HEAD,
- "patches": {
- "open": 1,
- "draft": 0,
- "archived": 0,
- "merged": 0,
- },
- "issues": {
- "open": 1,
- "closed": 0,
- },
- "id": RID,
- "seeding": 0,
- }
- ])
- );
- }
-
- #[tokio::test]
- async fn test_projects() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "name": "hello-world",
- "description": "Rad repository for tests",
- "defaultBranch": "master",
- "delegates": [
- {
- "id": DID,
- "alias": "seed"
- }
- ],
- "threshold": 1,
- "visibility": {
- "type": "public"
- },
- "head": HEAD,
- "patches": {
- "open": 1,
- "draft": 0,
- "archived": 0,
- "merged": 0,
- },
- "issues": {
- "open": 1,
- "closed": 0,
- },
- "id": RID,
- "seeding": 0,
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_not_found() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, "/projects/rad:z2u2CP3ZJzB7ZqE8jHrau19yjcfCQ").await;
-
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn test_projects_commits_root() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/commits")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!([
- {
- "id": HEAD,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz"
- },
- "summary": "Add another folder",
- "description": "",
- "parents": [
- "ee8d6a29304623a78ebfa5eeed5af674d0e58f83",
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673003014
- },
- },
- {
- "id": PARENT,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz"
- },
- "summary": "Add contributing file",
- "description": "",
- "parents": [
- "f604ce9fd5b7cc77b7609beda45ea8760bee78f7",
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673002014,
- },
- },
- {
- "id": INITIAL_COMMIT,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- },
- "summary": "Initial commit",
- "description": "",
- "parents": [],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673001014,
- },
- },
- ])
- );
- }
-
- #[tokio::test]
- async fn test_projects_commits() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/commits/{HEAD}")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "commit": {
- "id": HEAD,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz"
- },
- "summary": "Add another folder",
- "description": "",
- "parents": [
- "ee8d6a29304623a78ebfa5eeed5af674d0e58f83",
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673003014
- },
- },
- "diff": {
- "files": [
- {
- "state": "deleted",
- "path": "CONTRIBUTING",
- "diff": {
- "type": "plain",
- "hunks": [
- {
- "header": "@@ -1 +0,0 @@\n",
- "lines": [
- {
- "line": "Thank you very much!\n",
- "lineNo": 1,
- "type": "deletion",
- },
- ],
- "old": {
- "start": 1,
- "end": 2,
- },
- "new": {
- "start": 0,
- "end": 0,
- },
- },
- ],
- "stats": {
- "additions": 0,
- "deletions": 1,
- },
- "eof": "noneMissing",
- },
- "old": {
- "oid": "82eb77880c693655bce074e3dbbd9fa711dc018b",
- "mode": "blob",
- },
- },
- {
- "state": "added",
- "path": "README",
- "diff": {
- "type": "plain",
- "hunks": [
- {
- "header": "@@ -0,0 +1 @@\n",
- "lines": [
- {
- "line": "Hello World!\n",
- "lineNo": 1,
- "type": "addition",
- },
- ],
- "old": {
- "start": 0,
- "end": 0,
- },
- "new": {
- "start": 1,
- "end": 2,
- },
- },
- ],
- "stats": {
- "additions": 1,
- "deletions": 0,
- },
- "eof": "noneMissing",
- },
- "new": {
- "oid": "980a0d5f19a64b4b30a87d4206aade58726b60e3",
- "mode": "blob",
- },
- },
- {
- "state": "added",
- "path": "dir1/README",
- "diff": {
- "type": "plain",
- "hunks": [
- {
- "header": "@@ -0,0 +1 @@\n",
- "lines": [
- {
- "line": "Hello World from dir1!\n",
- "lineNo": 1,
- "type": "addition"
- }
- ],
- "old": {
- "start": 0,
- "end": 0,
- },
- "new": {
- "start": 1,
- "end": 2,
- },
- }
- ],
- "stats": {
- "additions": 1,
- "deletions": 0,
- },
- "eof": "noneMissing",
- },
- "new": {
- "oid": "1dd5654ca2d2cf9f33b14c92b5ca9e1d21a91ae1",
- "mode": "blob",
- },
- },
- ],
- "stats": {
- "filesChanged": 3,
- "insertions": 2,
- "deletions": 1
- }
- },
- "files": {
- "82eb77880c693655bce074e3dbbd9fa711dc018b": {
- "id": "82eb77880c693655bce074e3dbbd9fa711dc018b",
- "binary": false,
- "content": "Thank you very much!\n",
- },
- "980a0d5f19a64b4b30a87d4206aade58726b60e3": {
- "id": "980a0d5f19a64b4b30a87d4206aade58726b60e3",
- "binary": false,
- "content": "Hello World!\n",
- },
- "1dd5654ca2d2cf9f33b14c92b5ca9e1d21a91ae1": {
- "id": "1dd5654ca2d2cf9f33b14c92b5ca9e1d21a91ae1",
- "binary": false,
- "content": "Hello World from dir1!\n",
- },
- },
- "branches": [
- "refs/heads/master"
- ]
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_commits_not_found() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(
- &app,
- format!("/projects/{RID}/commits/ffffffffffffffffffffffffffffffffffffffff"),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn test_projects_stats() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/stats/tree/{HEAD}")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!(
- {
- "commits": 3,
- "branches": 1,
- "contributors": 1
- }
- )
- );
- }
-
- #[tokio::test]
- async fn test_projects_tree() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/tree/{HEAD}/")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "entries": [
- {
- "path": "dir1",
- "oid": "2d1c3cbfcf1d190d7fc77ac8f9e53db0e91a9ad3",
- "name": "dir1",
- "kind": "tree"
- },
- {
- "path": "README",
- "oid": "980a0d5f19a64b4b30a87d4206aade58726b60e3",
- "name": "README",
- "kind": "blob"
- }
- ],
- "lastCommit": {
- "id": HEAD,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz"
- },
- "summary": "Add another folder",
- "description": "",
- "parents": [
- "ee8d6a29304623a78ebfa5eeed5af674d0e58f83",
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673003014
- },
- },
- "name": "",
- "path": "",
- }
- )
- );
-
- let response = get(&app, format!("/projects/{RID}/tree/{HEAD}/dir1")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "entries": [
- {
- "path": "dir1/README",
- "oid": "1dd5654ca2d2cf9f33b14c92b5ca9e1d21a91ae1",
- "name": "README",
- "kind": "blob"
- }
- ],
- "lastCommit": {
- "id": HEAD,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz"
- },
- "summary": "Add another folder",
- "description": "",
- "parents": [
- "ee8d6a29304623a78ebfa5eeed5af674d0e58f83",
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673003014
- },
- },
- "name": "dir1",
- "path": "dir1",
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_tree_not_found() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(
- &app,
- format!("/projects/{RID}/tree/ffffffffffffffffffffffffffffffffffffffff"),
- )
- .await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
-
- let response = get(&app, format!("/projects/{RID}/tree/{HEAD}/unknown")).await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn test_projects_remotes_root() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/remotes")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!([
- {
- "id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
- "alias": CONTRIBUTOR_ALIAS,
- "heads": {
- "master": HEAD
- },
- "delegate": true
- }
- ])
- );
- }
-
- #[tokio::test]
- async fn test_projects_remotes() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(
- &app,
- format!("/projects/{RID}/remotes/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
- "heads": {
- "master": HEAD
- },
- "delegate": true
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_remotes_not_found() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(
- &app,
- format!("/projects/{RID}/remotes/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn test_projects_blob() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/blob/{HEAD}/README")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "binary": false,
- "name": "README",
- "path": "README",
- "lastCommit": {
- "id": HEAD,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz"
- },
- "summary": "Add another folder",
- "description": "",
- "parents": [
- "ee8d6a29304623a78ebfa5eeed5af674d0e58f83"
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673003014
- },
- },
- "content": "Hello World!\n",
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_blob_not_found() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/blob/{HEAD}/unknown")).await;
-
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn test_projects_readme() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/readme/{INITIAL_COMMIT}")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "binary": false,
- "name": "README",
- "path": "README",
- "lastCommit": {
- "id": INITIAL_COMMIT,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz"
- },
- "summary": "Initial commit",
- "description": "",
- "parents": [],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673001014
- },
- },
- "content": "Hello World!\n"
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_diff() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(
- &app,
- format!("/projects/{RID}/diff/{INITIAL_COMMIT}/{HEAD}"),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!({
- "diff": {
- "files": [
- {
- "state": "added",
- "path": "dir1/README",
- "diff": {
- "type": "plain",
- "hunks": [
- {
- "header": "@@ -0,0 +1 @@\n",
- "lines": [
- {
- "line": "Hello World from dir1!\n",
- "lineNo": 1,
- "type": "addition",
- },
- ],
- "old": {
- "start": 0,
- "end": 0,
- },
- "new": {
- "start": 1,
- "end": 2,
- },
- },
- ],
- "stats": {
- "additions": 1,
- "deletions": 0,
- },
- "eof": "noneMissing",
- },
- "new": {
- "oid": "1dd5654ca2d2cf9f33b14c92b5ca9e1d21a91ae1",
- "mode": "blob",
- },
- },
- ],
- "stats": {
- "filesChanged": 1,
- "insertions": 1,
- "deletions": 0,
- },
- },
- "files": {
- "1dd5654ca2d2cf9f33b14c92b5ca9e1d21a91ae1": {
- "id": "1dd5654ca2d2cf9f33b14c92b5ca9e1d21a91ae1",
- "binary": false,
- "content": "Hello World from dir1!\n",
- },
- },
- "commits": [
- {
- "id": HEAD,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- },
- "summary": "Add another folder",
- "description": "",
- "parents": [
- "ee8d6a29304623a78ebfa5eeed5af674d0e58f83"
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673003014,
- },
- },
- {
- "id": PARENT,
- "author": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- },
- "summary": "Add contributing file",
- "description": "",
- "parents": [
- "f604ce9fd5b7cc77b7609beda45ea8760bee78f7",
- ],
- "committer": {
- "name": "Alice Liddell",
- "email": "alice@radicle.xyz",
- "time": 1673002014,
- }
- }
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_issues_root() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(seed(tmp.path()));
- let response = get(&app, format!("/projects/{RID}/issues")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!([
- {
- "id": ISSUE_ID,
- "author": {
- "id": DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "Issue #1",
- "state": {
- "status": "open"
- },
- "assignees": [],
- "discussion": [
- {
- "id": ISSUE_ID,
- "author": {
- "id": DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "edits": [
- {
- "author": {
- "id": DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "embeds": [],
- "reactions": [],
- "timestamp": TIMESTAMP,
- "replyTo": null,
- "resolved": false,
- }
- ],
- "labels": []
- }
- ])
- );
- }
-
- #[tokio::test]
- async fn test_projects_issues_create() {
- const CREATED_ISSUE_ID: &str = "fcd0d5940b55df596cf8079fd1845903f1104bcd";
-
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
-
- create_session(ctx).await;
-
- let body = serde_json::to_vec(&json!({
- "title": "Issue #2",
- "description": "Change 'hello world' to 'hello everyone'",
- "labels": ["bug"],
- "embeds": [
- {
- "name": "example.html",
- "content": "data:image/png;base64,PGh0bWw+SGVsbG8gV29ybGQhPC9odG1sPg=="
- }
- ],
- "assignees": [],
- }))
- .unwrap();
-
- let response = post(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::CREATED);
- assert_eq!(
- response.json().await,
- json!({ "success": true, "id": CREATED_ISSUE_ID })
- );
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{CREATED_ISSUE_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CREATED_ISSUE_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "Issue #2",
- "state": {
- "status": "open",
- },
- "assignees": [],
- "discussion": [{
- "id": CREATED_ISSUE_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "timestamp": TIMESTAMP,
- "embeds": [
- {
- "name": "example.html",
- "content": "git:b62df2ec90365e3749cd4fa431cb844492908b84",
- },
- ],
- },
- ],
- "embeds": [
- {
- "name": "example.html",
- "content": "git:b62df2ec90365e3749cd4fa431cb844492908b84"
- }
- ],
- "reactions": [],
- "timestamp": TIMESTAMP,
- "replyTo": null,
- "resolved": false,
- }],
- "labels": [
- "bug",
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_issues_comment() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
-
- create_session(ctx).await;
-
- let body = serde_json::to_vec(&json!({
- "type": "comment",
- "body": "This is first-level comment",
- "embeds": [
- {
- "name": "image.jpg",
- "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC"
- }
- ],
- "replyTo": ISSUE_DISCUSSION_ID,
- }))
- .unwrap();
-
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- // Get ID to redact later in the test
- let response = response.json().await;
- let id = &response["id"];
- assert!(id.is_string());
-
- let body = serde_json::to_vec(&json!({
- "type": "comment.react",
- "id": ISSUE_DISCUSSION_ID,
- "reaction": "🚀",
- "active": true,
- }))
- .unwrap();
- patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- let body = serde_json::to_vec(&json!({
- "type": "comment.edit",
- "id": ISSUE_DISCUSSION_ID,
- "body": "EDIT: Change 'hello world' to 'hello anyone'",
- "embeds": [
- {
- "name":"image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc"
- }
- ]
- }))
- .unwrap();
-
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.success().await, true);
-
- let body = serde_json::to_vec(&json!({
- "type": "comment.redact",
- "id": id.as_str().unwrap(),
- }))
- .unwrap();
-
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.success().await, true);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": ISSUE_DISCUSSION_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "Issue #1",
- "state": {
- "status": "open",
- },
- "assignees": [],
- "discussion": [
- {
- "id": ISSUE_DISCUSSION_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "EDIT: Change 'hello world' to 'hello anyone'",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "EDIT: Change 'hello world' to 'hello anyone'",
- "timestamp": TIMESTAMP,
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- },
- ],
- },
- ],
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- }
- ],
- "reactions": [
- {
- "emoji": "🚀",
- "authors": [
- {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS,
- }
- ],
- },
- ],
- "timestamp": TIMESTAMP,
- "replyTo": null,
- "resolved": false,
- },
- ],
- "labels": [],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_issues_assign() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
-
- create_session(ctx).await;
-
- let body = serde_json::to_vec(&json!({
- "type": "assign",
- "assignees": [CONTRIBUTOR_DID],
- }))
- .unwrap();
-
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
- assert_eq!(response.status(), StatusCode::OK);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": ISSUE_DISCUSSION_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS,
- },
- "title": "Issue #1",
- "state": {
- "status": "open",
- },
- "assignees": [
- {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS,
- }
- ],
- "discussion": [
- {
- "id": ISSUE_DISCUSSION_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS,
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS,
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "embeds": [],
- "reactions": [],
- "timestamp": TIMESTAMP,
- "replyTo": null,
- "resolved": false,
- },
- ],
- "labels": [],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_issues_reply() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
-
- create_session(ctx).await;
-
- let body = serde_json::to_vec(&json!({
- "type": "comment",
- "body": "This is a reply to the first comment",
- "embeds": [
- {
- "name": "image.jpg",
- "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC"
- }
- ],
- "replyTo": ISSUE_DISCUSSION_ID,
- }))
- .unwrap();
-
- let _ = get(&app, format!("/projects/{CONTRIBUTOR_RID}/issues")).await;
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(response.success().await, true);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/issues/{ISSUE_DISCUSSION_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": ISSUE_DISCUSSION_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "Issue #1",
- "state": {
- "status": "open",
- },
- "assignees": [],
- "discussion": [
- {
- "id": ISSUE_DISCUSSION_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Change 'hello world' to 'hello everyone'",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "embeds": [],
- "reactions": [],
- "timestamp": TIMESTAMP,
- "replyTo": null,
- "resolved": false,
- },
- {
- "id": ISSUE_COMMENT_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "This is a reply to the first comment",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "This is a reply to the first comment",
- "timestamp": TIMESTAMP,
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- },
- ],
- },
- ],
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- }
- ],
- "reactions": [],
- "timestamp": TIMESTAMP,
- "replyTo": ISSUE_DISCUSSION_ID,
- "resolved": false,
- },
- ],
- "labels": [],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- let response = get(&app, format!("/projects/{CONTRIBUTOR_RID}/patches")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!([
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "reactions": [],
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- }
- ],
- }
- ])
- );
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!(
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "reactions": [],
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- }
- ],
- }
- )
- );
- }
-
- #[tokio::test]
- async fn test_projects_create_patches() {
- const CREATED_PATCH_ID: &str = "9aabc4055fd811f915c55e9a6ea9f525aa3e88f2";
-
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
-
- create_session(ctx).await;
-
- let body = serde_json::to_vec(&json!({
- "title": "Update README",
- "description": "Do some changes to README",
- "target": INITIAL_COMMIT,
- "oid": HEAD,
- "labels": [],
- }))
- .unwrap();
-
- let response = post(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::CREATED);
- assert_eq!(
- response.json().await,
- json!(
- {
- "success": true,
- "id": CREATED_PATCH_ID,
- }
- )
- );
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CREATED_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(
- response.json().await,
- json!(
- {
- "id": CREATED_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "Update README",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CREATED_PATCH_ID,
- "reactions": [],
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "Do some changes to README",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Do some changes to README",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "base": INITIAL_COMMIT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- }
- ],
- }
- )
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_assign() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let body = serde_json::to_vec(&json!({
- "type": "assign",
- "assignees": [CONTRIBUTOR_DID]
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [
- {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS,
- }
- ],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- },
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_label() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let body = serde_json::to_vec(&json!({
- "type": "label",
- "labels": ["bug","design"],
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [
- "bug",
- "design"
- ],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- },
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_revisions() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let body = serde_json::to_vec(&json!({
- "type": "revision",
- "description": "This is a new revision",
- "base": PARENT,
- "oid": HEAD,
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- },
- {
- "id": "cccf3b0675220f25b054b6625d84611cb6506d9a",
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "This is a new revision",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "This is a new revision",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- }
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_edit() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let body = serde_json::to_vec(&json!({
- "type": "edit",
- "title": "This is a updated title",
- "description": "Let's write some description",
- "target": "delegates",
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "This is a updated title",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "reactions": [],
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- },
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_revisions_edit() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let body = serde_json::to_vec(&json!({
- "type": "revision.edit",
- "revision": CONTRIBUTOR_PATCH_ID,
- "description": "Let's change the description a bit",
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let body = serde_json::to_vec(&json!({
- "type": "revision.react",
- "revision": CONTRIBUTOR_PATCH_ID,
- "reaction": "🚀",
- "location": {
- "commit": INITIAL_COMMIT,
- "path": "./README.md",
- "new": {
- "type": "lines",
- "range": {
- "start": 0,
- "end": 1
- }
- }
- },
- "active": true,
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let body = serde_json::to_vec(&json!({
- "type": "revision.react",
- "revision": CONTRIBUTOR_PATCH_ID,
- "reaction": "🙏",
- "location": null,
- "active": true,
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "Let's change the description a bit",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "Let's change the description a bit",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [
- {
- "emoji": "🙏",
- "authors": [
- {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- }
- ],
- },
- {
- "location": {
- "commit": INITIAL_COMMIT,
- "path": "./README.md",
- "old": null,
- "new": {
- "type": "lines",
- "range": {
- "start": 0,
- "end": 1
- }
- }
- },
- "emoji": "🚀",
- "authors": [
- {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- }
- ]
- },
- ],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- },
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_discussions() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let thread_body = serde_json::to_vec(&json!({
- "type": "revision.comment",
- "revision": CONTRIBUTOR_PATCH_ID,
- "body": "This is a root level comment",
- "embeds": [
- {
- "name": "image.jpg",
- "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC"
- }
- ],
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(thread_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let comment_id = response.id().await.to_string();
- let comment_react_body = serde_json::to_vec(&json!({
- "type": "revision.comment.react",
- "revision": CONTRIBUTOR_PATCH_ID,
- "comment": comment_id,
- "reaction": "🚀",
- "active": true
- }))
- .unwrap();
- patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(comment_react_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- let comment_edit = serde_json::to_vec(&json!({
- "type": "revision.comment.edit",
- "revision": CONTRIBUTOR_PATCH_ID,
- "comment": comment_id,
- "body": "EDIT: This is a root level comment",
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- }
- ],
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(comment_edit)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
- let reply_body = serde_json::to_vec(&json!({
- "type": "revision.comment",
- "revision": CONTRIBUTOR_PATCH_ID,
- "body": "This is a root level comment",
- "replyTo": comment_id,
- "embeds": [],
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(reply_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
- let comment_id_2 = response.id().await.to_string();
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [
- {
- "id": comment_id,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "EDIT: This is a root level comment",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "This is a root level comment",
- "timestamp": TIMESTAMP,
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- },
- ],
- },
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "EDIT: This is a root level comment",
- "timestamp": TIMESTAMP,
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- },
- ],
- },
- ],
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- }
- ],
- "reactions": [
- {
- "emoji": "🚀",
- "authors": [
- {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- }
- ],
- },
- ],
- "timestamp": TIMESTAMP,
- "replyTo": null,
- "location": null,
- "resolved": false,
- },
- {
- "id": comment_id_2,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "This is a root level comment",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "This is a root level comment",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "embeds": [],
- "reactions": [],
- "timestamp": TIMESTAMP,
- "replyTo": comment_id,
- "location": null,
- "resolved": false,
- },
- ],
- "timestamp": TIMESTAMP,
- "reviews": [],
- },
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_reviews() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let thread_body = serde_json::to_vec(&json!({
- "type": "review",
- "revision": CONTRIBUTOR_PATCH_ID,
- "summary": "A small review",
- "verdict": "accept",
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(thread_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let review_id = response.id().await.to_string();
- let review_comment_body = serde_json::to_vec(&json!({
- "type": "review.comment",
- "review": review_id,
- "body": "This is a comment on a review",
- "embeds": [
- {
- "name": "image.jpg",
- "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC"
- }
- ],
- "location": {
- "commit": HEAD,
- "path": "README.md",
- "new": {
- "type": "lines",
- "range": {
- "start": 2,
- "end": 4
- }
- }
- }
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(review_comment_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- let comment_id = response.id().await.to_string();
- let review_comment_edit_body = serde_json::to_vec(&json!({
- "type": "review.comment.edit",
- "review": review_id,
- "comment": comment_id,
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- }
- ],
- "body": "EDIT: This is a comment on a review",
- }))
- .unwrap();
- patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(review_comment_edit_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- let review_react_body = serde_json::to_vec(&json!({
- "type": "review.comment.react",
- "review": review_id,
- "comment": comment_id,
- "reaction": "🚀",
- "active": true
- }))
- .unwrap();
- patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(review_react_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- let review_resolve_body = serde_json::to_vec(&json!({
- "type": "review.comment.resolve",
- "review": review_id,
- "comment": comment_id,
- }))
- .unwrap();
- patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(review_resolve_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": { "status": "open" },
- "target": "delegates",
- "labels": [],
- "merges": [],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [
- {
- "id": "140a44a4eac2cdb74b2f5f95a9dce97847eb9636",
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "verdict": "accept",
- "summary": "A small review",
- "comments": [
- {
- "id": "0dcfca53416761cf975cc4cd6d452790cee06b49",
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "EDIT: This is a comment on a review",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "This is a comment on a review",
- "timestamp": 1671125284,
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- },
- ],
- },
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "EDIT: This is a comment on a review",
- "timestamp": 1671125284,
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- },
- ],
- },
- ],
- "embeds": [
- {
- "name": "image.jpg",
- "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc",
- },
- ],
- "reactions": [
- {
- "emoji": "🚀",
- "authors": [
- {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- }
- ],
- },
- ],
- "timestamp": 1671125284,
- "replyTo": null,
- "location": {
- "commit": HEAD,
- "path": "README.md",
- "old": null,
- "new": {
- "type": "lines",
- "range": {
- "start": 2,
- "end": 4,
- },
- },
- },
- "resolved": true,
- }
- ],
- "timestamp": 1671125284,
- },
- ],
- },
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_patches_merges() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = contributor(tmp.path());
- let app = super::router(ctx.to_owned());
- create_session(ctx).await;
- let thread_body = serde_json::to_vec(&json!({
- "type": "merge",
- "revision": CONTRIBUTOR_PATCH_ID,
- "commit": PARENT,
- }))
- .unwrap();
- let response = patch(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- Some(Body::from(thread_body)),
- Some(SESSION_ID.to_string()),
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- let response = get(
- &app,
- format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"),
- )
- .await;
-
- assert_eq!(
- response.json().await,
- json!({
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "title": "A new `hello world`",
- "state": {
- "status": "merged",
- "revision": CONTRIBUTOR_PATCH_ID,
- "commit": PARENT,
- },
- "target": "delegates",
- "labels": [],
- "merges": [{
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "commit": PARENT,
- "timestamp": TIMESTAMP,
- "revision": CONTRIBUTOR_PATCH_ID,
- }],
- "assignees": [],
- "revisions": [
- {
- "id": CONTRIBUTOR_PATCH_ID,
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "description": "change `hello world` in README to something else",
- "edits": [
- {
- "author": {
- "id": CONTRIBUTOR_DID,
- "alias": CONTRIBUTOR_ALIAS
- },
- "body": "change `hello world` in README to something else",
- "timestamp": TIMESTAMP,
- "embeds": [],
- },
- ],
- "reactions": [],
- "base": PARENT,
- "oid": HEAD,
- "refs": [
- "refs/heads/master",
- ],
- "discussions": [],
- "timestamp": TIMESTAMP,
- "reviews": [],
- },
- ],
- })
- );
- }
-
- #[tokio::test]
- async fn test_projects_private() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = seed(tmp.path());
- let app = super::router(ctx.to_owned());
-
- // Check that the repo exists.
- ctx.profile()
- .storage
- .repository(RID_PRIVATE.parse().unwrap())
- .unwrap();
-
- let response = get(&app, format!("/projects/{RID_PRIVATE}")).await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
-
- let response = get(&app, format!("/projects/{RID_PRIVATE}/patches")).await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
-
- let response = get(&app, format!("/projects/{RID_PRIVATE}/issues")).await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
-
- let response = get(&app, format!("/projects/{RID_PRIVATE}/commits")).await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
-
- let response = get(&app, format!("/projects/{RID_PRIVATE}/remotes")).await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-}
diff --git a/radicle-httpd/src/api/v1/sessions.rs b/radicle-httpd/src/api/v1/sessions.rs
deleted file mode 100644
index 5d794cb1..00000000
--- a/radicle-httpd/src/api/v1/sessions.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-use std::iter::repeat_with;
-
-use axum::extract::State;
-use axum::response::IntoResponse;
-use axum::routing::{post, put};
-use axum::{Json, Router};
-use axum_auth::AuthBearer;
-use hyper::StatusCode;
-use radicle::crypto::{PublicKey, Signature};
-use serde::{Deserialize, Serialize};
-use time::OffsetDateTime;
-
-use crate::api::auth::{self, AuthState, Session};
-use crate::api::error::Error;
-use crate::api::json;
-use crate::api::Context;
-use crate::axum_extra::Path;
-
-pub fn router(ctx: Context) -> Router {
- Router::new()
- .route("/sessions", post(session_create_handler))
- .route(
- "/sessions/:id",
- put(session_signin_handler)
- .get(session_handler)
- .delete(session_delete_handler),
- )
- .with_state(ctx)
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-struct AuthChallenge {
- sig: Signature,
- pk: PublicKey,
-}
-
-/// Create session.
-/// `POST /sessions`
-async fn session_create_handler(State(ctx): State<Context>) -> impl IntoResponse {
- let mut rng = fastrand::Rng::new();
- let session_id = repeat_with(|| rng.alphanumeric())
- .take(32)
- .collect::<String>();
- let signer = ctx.profile.signer().map_err(Error::from)?;
- let session = Session {
- status: AuthState::Unauthorized,
- public_key: *signer.public_key(),
- alias: ctx.profile.config.node.alias.clone(),
- issued_at: OffsetDateTime::now_utc(),
- expires_at: OffsetDateTime::now_utc()
- .checked_add(auth::UNAUTHORIZED_SESSIONS_EXPIRATION)
- .unwrap(),
- };
- let mut sessions = ctx.sessions.write().await;
- sessions.insert(session_id.clone(), session.clone());
-
- Ok::<_, Error>((
- StatusCode::CREATED,
- Json(json::session(session_id, &session)),
- ))
-}
-
-/// Get a session.
-/// `GET /sessions/:id`
-async fn session_handler(
- State(ctx): State<Context>,
- Path(session_id): Path<String>,
-) -> impl IntoResponse {
- let sessions = ctx.sessions.read().await;
- let session = sessions.get(&session_id).ok_or(Error::NotFound)?;
-
- Ok::<_, Error>(Json(json::session(session_id, session)))
-}
-
-/// Update session.
-/// `PUT /sessions/:id`
-async fn session_signin_handler(
- State(ctx): State<Context>,
- Path(session_id): Path<String>,
- Json(request): Json<AuthChallenge>,
-) -> impl IntoResponse {
- let mut sessions = ctx.sessions.write().await;
- let session = sessions.get_mut(&session_id).ok_or(Error::NotFound)?;
- if session.status == AuthState::Unauthorized {
- if session.public_key != request.pk {
- return Err(Error::Auth("Invalid public key"));
- }
- if session.expires_at <= OffsetDateTime::now_utc() {
- return Err(Error::Auth("Session expired"));
- }
- let payload = format!("{}:{}", session_id, request.pk);
- request
- .pk
- .verify(payload.as_bytes(), &request.sig)
- .map_err(Error::from)?;
- session.status = AuthState::Authorized;
- session.expires_at = OffsetDateTime::now_utc()
- .checked_add(auth::AUTHORIZED_SESSIONS_EXPIRATION)
- .unwrap();
-
- return Ok::<_, Error>(Json(json!({ "success": true })));
- }
-
- Err(Error::Auth("Session already authorized"))
-}
-
-/// Delete session.
-/// `DELETE /sessions/:id`
-async fn session_delete_handler(
- State(ctx): State<Context>,
- AuthBearer(token): AuthBearer,
- Path(session_id): Path<String>,
-) -> impl IntoResponse {
- if token != session_id {
- return Err(Error::Auth("Not authorized to delete this session"));
- }
- let mut sessions = ctx.sessions.write().await;
- sessions.remove_entry(&token).ok_or(Error::NotFound)?;
-
- Ok::<_, Error>(Json(json!({ "success": true })))
-}
-
-#[cfg(test)]
-mod routes {
- use crate::commands::web::{sign, SessionInfo};
- use axum::body::Body;
- use axum::http::StatusCode;
-
- use crate::api::auth::{AuthState, Session};
- use crate::test::{self, get, post, put};
-
- #[tokio::test]
- async fn test_session() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = test::seed(tmp.path());
- let app = super::router(ctx.to_owned());
-
- // Create session.
- let response = post(&app, "/sessions", None, None).await;
- let status = response.status();
- let json = response.json().await;
- let session_info: SessionInfo = serde_json::from_value(json).unwrap();
-
- assert_eq!(status, StatusCode::CREATED);
-
- // Check that an unauthorized session has been created.
- let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
- let status = response.status();
- let json = response.json().await;
- let body: Session = serde_json::from_value(json).unwrap();
-
- assert_eq!(status, StatusCode::OK);
- assert_eq!(body.status, AuthState::Unauthorized);
-
- // Create request body
- let signer = ctx.profile.signer().unwrap();
- let signature = sign(signer, &session_info).unwrap();
- let body = serde_json::to_vec(&super::AuthChallenge {
- sig: signature,
- pk: session_info.public_key,
- })
- .unwrap();
-
- let response = put(
- &app,
- format!("/sessions/{}", session_info.session_id),
- Some(Body::from(body)),
- None,
- )
- .await;
-
- assert_eq!(response.status(), StatusCode::OK);
-
- // Check that session has been authorized.
- let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
- let status = response.status();
- let json = response.json().await;
- let body: Session = serde_json::from_value(json).unwrap();
-
- assert_eq!(status, StatusCode::OK);
- assert_eq!(body.status, AuthState::Authorized);
- }
-}
diff --git a/radicle-httpd/src/api/v1/stats.rs b/radicle-httpd/src/api/v1/stats.rs
deleted file mode 100644
index 056f5e2d..00000000
--- a/radicle-httpd/src/api/v1/stats.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use axum::extract::State;
-use axum::response::IntoResponse;
-use axum::routing::get;
-use axum::{Json, Router};
-use serde_json::json;
-
-use radicle::storage::ReadStorage;
-
-use crate::api::error::Error;
-use crate::api::Context;
-
-pub fn router(ctx: Context) -> Router {
- Router::new()
- .route("/stats", get(stats_handler))
- .with_state(ctx)
-}
-
-/// Return the stats for the node.
-/// `GET /stats`
-async fn stats_handler(State(ctx): State<Context>) -> impl IntoResponse {
- let total = ctx.profile.storage.repositories()?.len();
-
- Ok::<_, Error>(Json(json!({ "repos": { "total": total } })))
-}
-
-#[cfg(test)]
-mod routes {
- use axum::http::StatusCode;
- use serde_json::json;
-
- use crate::test::{self, get};
-
- #[tokio::test]
- async fn test_stats() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(test::seed(tmp.path()));
- let response = get(&app, "/stats").await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(response.json().await, json!({ "repos": { "total": 2 } }));
- }
-}
diff --git a/radicle-httpd/src/axum_extra.rs b/radicle-httpd/src/axum_extra.rs
deleted file mode 100644
index 3bdfcb6a..00000000
--- a/radicle-httpd/src/axum_extra.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-use axum::extract::path::ErrorKind;
-use axum::extract::rejection::{PathRejection, QueryRejection};
-use axum::extract::FromRequestParts;
-use axum::http::request::Parts;
-use axum::http::{header, StatusCode};
-use axum::response::IntoResponse;
-use axum::{async_trait, Json};
-
-use serde::de::DeserializeOwned;
-use serde::Serialize;
-
-pub struct Path<T>(pub T);
-
-#[async_trait]
-impl<S, T> FromRequestParts<S> for Path<T>
-where
- T: DeserializeOwned + Send,
- S: Send + Sync,
-{
- type Rejection = (StatusCode, axum::Json<Error>);
-
- async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
- match axum::extract::Path::<T>::from_request_parts(req, state).await {
- Ok(value) => Ok(Self(value.0)),
- Err(rejection) => {
- let status = StatusCode::BAD_REQUEST;
- let body = match rejection {
- PathRejection::FailedToDeserializePathParams(inner) => {
- let kind = inner.into_kind();
- match &kind {
- ErrorKind::Message(msg) => Json(Error {
- success: false,
- error: msg.to_string(),
- }),
- _ => Json(Error {
- success: false,
- error: kind.to_string(),
- }),
- }
- }
- _ => Json(Error {
- success: false,
- error: format!("{rejection}"),
- }),
- };
-
- Err((status, body))
- }
- }
- }
-}
-
-#[derive(Default)]
-pub struct Query<T>(pub T);
-
-#[async_trait]
-impl<S, T> FromRequestParts<S> for Query<T>
-where
- T: DeserializeOwned + Send,
- S: Send + Sync,
-{
- type Rejection = (StatusCode, axum::Json<Error>);
-
- async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
- match axum::extract::Query::<T>::from_request_parts(req, state).await {
- Ok(value) => Ok(Self(value.0)),
- Err(rejection) => {
- let status = StatusCode::BAD_REQUEST;
- let body = match rejection {
- QueryRejection::FailedToDeserializeQueryString(inner) => Json(Error {
- success: false,
- error: inner.to_string(),
- }),
- _ => Json(Error {
- success: false,
- error: format!("{rejection}"),
- }),
- };
-
- Err((status, body))
- }
- }
- }
-}
-
-#[derive(Serialize)]
-pub struct Error {
- success: bool,
- error: String,
-}
-
-/// Add a Cache-Control header that marks the response as immutable and
-/// instructs clients to cache the response for 7 days.
-pub fn immutable_response(data: impl serde::Serialize) -> impl IntoResponse {
- (
- [(header::CACHE_CONTROL, "public, max-age=604800, immutable")],
- Json(data),
- )
-}
diff --git a/radicle-httpd/src/bin/rad-web.rs b/radicle-httpd/src/bin/rad-web.rs
deleted file mode 100644
index 49b01ef5..00000000
--- a/radicle-httpd/src/bin/rad-web.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-use radicle_cli::terminal as term;
-use radicle_httpd::commands::web as rad_web;
-
-fn main() {
- term::run_command_args::<rad_web::Options, _>(
- rad_web::HELP,
- rad_web::run,
- std::env::args_os().skip(1).collect(),
- )
-}
diff --git a/radicle-httpd/src/cache.rs b/radicle-httpd/src/cache.rs
deleted file mode 100644
index 5b88f9ae..00000000
--- a/radicle-httpd/src/cache.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-use std::num::NonZeroUsize;
-use std::sync::Arc;
-
-use lru::LruCache;
-use tokio::sync::Mutex;
-
-use radicle::prelude::RepoId;
-use radicle_surf::Oid;
-
-#[derive(Clone)]
-pub struct Cache {
- pub tree: Arc<Mutex<LruCache<(RepoId, Oid, String), serde_json::Value>>>,
-}
-
-impl Cache {
- /// Creates a new cache of the given size.
- pub fn new(size: NonZeroUsize) -> Self {
- Cache {
- tree: Arc::new(Mutex::new(LruCache::new(size))),
- }
- }
-}
diff --git a/radicle-httpd/src/commands.rs b/radicle-httpd/src/commands.rs
deleted file mode 100644
index 112ad9c2..00000000
--- a/radicle-httpd/src/commands.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-//! Extra CLI commands relating to HTTPd.
-pub mod web;
diff --git a/radicle-httpd/src/commands/web.rs b/radicle-httpd/src/commands/web.rs
deleted file mode 100644
index 866f9d65..00000000
--- a/radicle-httpd/src/commands/web.rs
+++ /dev/null
@@ -1,233 +0,0 @@
-use std::ffi::OsString;
-use std::net::{IpAddr, Ipv4Addr, SocketAddr};
-use std::process::Command;
-use std::thread::sleep;
-use std::time::Duration;
-
-use anyhow::{anyhow, Context};
-use serde::{Deserialize, Serialize};
-use url::{Position, Url};
-
-use radicle::crypto::{PublicKey, Signature, Signer};
-
-use radicle_cli::terminal as term;
-use radicle_cli::terminal::args::{Args, Error, Help};
-
-pub const HELP: Help = Help {
- name: "web",
- description: "Run the HTTP daemon and connect the web explorer to it",
- version: env!("RADICLE_VERSION"),
- usage: r#"
-Usage
-
- rad web [<option>...] [<explorer-url>]
-
- Runs the Radicle HTTP Daemon and opens a Radicle web explorer to authenticate with it.
-
-Options
-
- --listen, -l <addr> Address to bind the HTTP daemon to (default: 127.0.0.1:8080)
- --connect, -c [<addr>] Connect the explorer to an already running daemon (default: 127.0.0.1:8080)
- --path, -p <path> Path to be opened in the explorer after authentication
- --[no-]open Open the authentication URL automatically (default: open)
- --help Print help
-"#,
-};
-
-#[derive(Debug, Clone, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SessionInfo {
- pub session_id: String,
- pub public_key: PublicKey,
-}
-
-#[derive(Debug)]
-pub struct Options {
- pub app_url: Url,
- pub listen: SocketAddr,
- pub path: Option<String>,
- pub connect: Option<SocketAddr>,
- pub open: bool,
-}
-
-impl Args for Options {
- fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
- use lexopt::prelude::*;
-
- let mut parser = lexopt::Parser::from_args(args);
- let mut listen = None;
- let mut connect = None;
- let mut path = None;
- // SAFETY: This is a valid URL.
- #[allow(clippy::unwrap_used)]
- let mut app_url = Url::parse("https://app.radicle.xyz").unwrap();
- let mut open = true;
-
- while let Some(arg) = parser.next()? {
- match arg {
- Long("listen") | Short('l') if listen.is_none() => {
- let val = parser.value()?;
- listen = Some(term::args::socket_addr(&val)?);
- }
- Long("path") | Short('p') if path.is_none() => {
- let val = parser.value()?;
- path = Some(term::args::string(&val));
- }
- Long("connect") | Short('c') if connect.is_none() => {
- if let Ok(val) = parser.value() {
- connect = Some(term::args::socket_addr(&val)?);
- } else {
- connect = Some(SocketAddr::new(
- IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
- 8080,
- ));
- }
- }
- Long("open") => open = true,
- Long("no-open") => open = false,
- Long("help") | Short('h') => {
- return Err(Error::Help.into());
- }
- Value(val) => {
- let val = val.to_string_lossy();
- app_url = Url::parse(val.as_ref()).context("invalid explorer URL supplied")?;
- }
- _ => {
- return Err(anyhow!(arg.unexpected()));
- }
- }
- }
-
- Ok((
- Options {
- open,
- app_url,
- listen: listen.unwrap_or(SocketAddr::new(
- IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
- 8080,
- )),
- path,
- connect,
- },
- vec![],
- ))
- }
-}
-
-pub fn sign(signer: Box<dyn Signer>, session: &SessionInfo) -> Result<Signature, anyhow::Error> {
- signer
- .try_sign(format!("{}:{}", session.session_id, session.public_key).as_bytes())
- .map_err(anyhow::Error::from)
-}
-
-pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
- let profile = ctx.profile()?;
- let runtime_and_handle = if options.connect.is_none() {
- tracing_subscriber::fmt::init();
-
- let runtime = tokio::runtime::Builder::new_multi_thread()
- .enable_all()
- .build()
- .expect("failed to create threaded runtime");
- let httpd_handle = runtime.spawn(crate::run(crate::Options {
- aliases: Default::default(),
- listen: options.listen,
- cache: None,
- }));
- Some((runtime, httpd_handle))
- } else {
- None
- };
-
- let mut retries = 30;
- let connect = options.connect.unwrap_or(options.listen);
- let response = loop {
- retries -= 1;
- sleep(Duration::from_millis(100));
-
- match ureq::post(&format!("http://{connect}/api/v1/sessions")).call() {
- Ok(response) => {
- break response;
- }
- Err(err) => {
- if err.kind() == ureq::ErrorKind::ConnectionFailed && retries > 0 {
- continue;
- } else {
- anyhow::bail!(err);
- }
- }
- }
- };
-
- let session = response.into_json::<SessionInfo>()?;
- let signer = profile.signer()?;
- let signature = sign(signer, &session)?;
-
- let mut auth_url = options.app_url.clone();
- auth_url
- .path_segments_mut()
- .map_err(|_| anyhow!("URL not supported"))?
- .push("session")
- .push(&session.session_id);
-
- auth_url
- .query_pairs_mut()
- .append_pair("pk", &session.public_key.to_string())
- .append_pair("sig", &signature.to_string())
- .append_pair("addr", &connect.to_string());
-
- let pathname = radicle::rad::cwd().ok().and_then(|(_, rid)| {
- Url::parse(
- &profile
- .config
- .public_explorer
- .url(options.listen, rid)
- .to_string(),
- )
- .map(|x| x[Position::BeforePath..].to_string())
- .ok()
- });
- if let Some(path) = options.path.or(pathname) {
- auth_url.query_pairs_mut().append_pair("path", &path);
- }
-
- if options.open {
- #[cfg(any(target_os = "freebsd", target_os = "windows"))]
- let cmd_name = "echo";
- #[cfg(target_os = "macos")]
- let cmd_name = "open";
- #[cfg(target_os = "linux")]
- let cmd_name = "xdg-open";
-
- let mut cmd = Command::new(cmd_name);
- match cmd.arg(auth_url.as_str()).spawn() {
- Ok(mut child) => match child.wait() {
- Ok(exit_status) => {
- if exit_status.success() {
- term::success!("Opened {auth_url}");
- } else {
- term::info!("Visit {auth_url} to connect");
- }
- }
- Err(_) => {
- term::info!("Visit {auth_url} to connect");
- }
- },
- Err(_) => {
- term::error(format!("Could not open web browser via `{cmd_name}`"));
- term::hint("Use `rad web --no-open` if this continues");
- term::info!("Visit {auth_url} to connect");
- }
- }
- } else {
- term::info!("Visit {auth_url} to connect");
- }
-
- if let Some((runtime, httpd_handle)) = runtime_and_handle {
- runtime
- .block_on(httpd_handle)?
- .context("httpd server error")?;
- }
-
- Ok(())
-}
diff --git a/radicle-httpd/src/error.rs b/radicle-httpd/src/error.rs
deleted file mode 100644
index 1dc002a9..00000000
--- a/radicle-httpd/src/error.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-use std::process::ExitStatus;
-
-use axum::http;
-use axum::response::{IntoResponse, Response};
-
-/// Errors relating to the Git backend.
-#[derive(Debug, thiserror::Error)]
-pub enum GitError {
- /// The entity was not found.
- #[error("not found")]
- NotFound,
-
- /// I/O error.
- #[error("i/o error: {0}")]
- Io(#[from] std::io::Error),
-
- /// The service is not available.
- #[error("service '{0}' not available")]
- ServiceUnavailable(&'static str),
-
- /// Invalid identifier.
- #[error("invalid radicle identifier: {0}")]
- Id(#[from] radicle::identity::IdError),
-
- /// Storage error.
- #[error("storage: {0}")]
- Storage(#[from] radicle::storage::Error),
-
- /// Repository error.
- #[error("repository: {0}")]
- Repository(#[from] radicle::storage::RepositoryError),
-
- /// Git backend error.
- #[error("git-http-backend: exited with code {0}")]
- BackendExited(ExitStatus),
-
- /// Git backend error.
- #[error("git-http-backend: invalid header returned: {0:?}")]
- BackendHeader(String),
-
- /// HeaderName error.
- #[error(transparent)]
- InvalidHeaderName(#[from] axum::http::header::InvalidHeaderName),
-
- /// HeaderValue error.
- #[error(transparent)]
- InvalidHeaderValue(#[from] axum::http::header::InvalidHeaderValue),
-}
-
-impl GitError {
- pub fn status(&self) -> http::StatusCode {
- match self {
- GitError::ServiceUnavailable(_) => http::StatusCode::SERVICE_UNAVAILABLE,
- GitError::Id(_) => http::StatusCode::NOT_FOUND,
- GitError::NotFound => http::StatusCode::NOT_FOUND,
- _ => http::StatusCode::INTERNAL_SERVER_ERROR,
- }
- }
-}
-
-impl IntoResponse for GitError {
- fn into_response(self) -> Response {
- tracing::error!("{}", self);
-
- self.status().into_response()
- }
-}
-
-/// Errors relating to the `/raw` route.
-#[derive(Debug, thiserror::Error)]
-pub enum RawError {
- /// Surf error.
- #[error(transparent)]
- Surf(#[from] radicle_surf::Error),
-
- /// Git error.
- #[error(transparent)]
- Git(#[from] radicle::git::ext::Error),
-
- /// Radicle Storage error.
- #[error(transparent)]
- Storage(#[from] radicle::storage::Error),
-
- /// Repository error.
- #[error(transparent)]
- Repository(#[from] radicle::storage::RepositoryError),
-
- /// Http Headers error.
- #[error(transparent)]
- Headers(#[from] http::header::InvalidHeaderValue),
-
- /// Surf file error.
- #[error(transparent)]
- SurfFile(#[from] radicle_surf::fs::error::File),
-
- /// The entity was not found.
- #[error("not found")]
- NotFound,
-}
-
-impl RawError {
- pub fn status(&self) -> http::StatusCode {
- match self {
- RawError::SurfFile(_) | RawError::NotFound => http::StatusCode::NOT_FOUND,
- _ => http::StatusCode::INTERNAL_SERVER_ERROR,
- }
- }
-}
-
-impl IntoResponse for RawError {
- fn into_response(self) -> Response {
- tracing::error!("{}", self);
-
- self.status().into_response()
- }
-}
diff --git a/radicle-httpd/src/git.rs b/radicle-httpd/src/git.rs
deleted file mode 100644
index c2f0de8f..00000000
--- a/radicle-httpd/src/git.rs
+++ /dev/null
@@ -1,252 +0,0 @@
-use std::collections::HashMap;
-use std::io::prelude::*;
-use std::net::SocketAddr;
-use std::path::Path;
-use std::process::{Command, Stdio};
-use std::sync::Arc;
-use std::{io, net, str};
-
-use axum::body::Bytes;
-use axum::extract::{ConnectInfo, Path as AxumPath, RawQuery, State};
-use axum::http::header::HeaderName;
-use axum::http::{HeaderMap, Method, StatusCode};
-use axum::response::IntoResponse;
-use axum::routing::any;
-use axum::Router;
-use flate2::write::GzDecoder;
-use hyper::body::Buf as _;
-
-use radicle::identity::RepoId;
-use radicle::profile::Profile;
-use radicle::storage::{ReadRepository, ReadStorage};
-
-use crate::error::GitError as Error;
-
-pub fn router(profile: Arc<Profile>, aliases: HashMap<String, RepoId>) -> Router {
- Router::new()
- .route("/:project/*request", any(git_handler))
- .with_state((profile, aliases))
-}
-
-async fn git_handler(
- State((profile, aliases)): State<(Arc<Profile>, HashMap<String, RepoId>)>,
- AxumPath((project, request)): AxumPath<(String, String)>,
- method: Method,
- headers: HeaderMap,
- ConnectInfo(remote): ConnectInfo<SocketAddr>,
- query: RawQuery,
- body: Bytes,
-) -> impl IntoResponse {
- let query = query.0.unwrap_or_default();
- let name = project.strip_suffix(".git").unwrap_or(&project);
- let rid: RepoId = match name.parse() {
- Ok(rid) => rid,
- Err(_) => {
- let Some(rid) = aliases.get(name) else {
- return Err(Error::NotFound);
- };
- *rid
- }
- };
-
- let (status, headers, body) = git_http_backend(
- &profile, method, headers, body, remote, rid, &request, query,
- )
- .await?;
-
- let mut response_headers = HeaderMap::new();
- for (name, vec) in headers.iter() {
- for value in vec {
- let header: HeaderName = name.try_into()?;
- response_headers.insert(header, value.parse()?);
- }
- }
-
- Ok::<_, Error>((status, response_headers, body))
-}
-
-async fn git_http_backend(
- profile: &Profile,
- method: Method,
- headers: HeaderMap,
- mut body: Bytes,
- remote: net::SocketAddr,
- id: RepoId,
- path: &str,
- query: String,
-) -> Result<(StatusCode, HashMap<String, Vec<String>>, Vec<u8>), Error> {
- let git_dir = radicle::storage::git::paths::repository(&profile.storage, &id);
- let content_type =
- if let Some(Ok(content_type)) = headers.get("Content-Type").map(|h| h.to_str()) {
- content_type
- } else {
- ""
- };
-
- // Don't allow cloning of private repositories.
- let doc = profile.storage.repository(id)?.identity_doc()?;
- if doc.visibility.is_private() {
- return Err(Error::NotFound);
- }
-
- // Reject push requests.
- match (path, query.as_str()) {
- ("git-receive-pack", _) | (_, "service=git-receive-pack") => {
- return Err(Error::ServiceUnavailable("git-receive-pack"));
- }
- _ => {}
- };
-
- tracing::debug!("id: {:?}", id);
- tracing::debug!("headers: {:?}", headers);
- tracing::debug!("path: {:?}", path);
- tracing::debug!("method: {:?}", method.as_str());
- tracing::debug!("remote: {:?}", remote.to_string());
-
- let mut cmd = Command::new("git");
- let mut child = cmd
- .arg("http-backend")
- .env("REQUEST_METHOD", method.as_str())
- .env("GIT_PROJECT_ROOT", git_dir)
- // "The GIT_HTTP_EXPORT_ALL environmental variable may be passed to git-http-backend to bypass
- // the check for the "git-daemon-export-ok" file in each repository before allowing export of
- // that repository."
- .env("GIT_HTTP_EXPORT_ALL", String::default())
- .env("PATH_INFO", Path::new("/").join(path))
- .env("CONTENT_TYPE", content_type)
- .env("QUERY_STRING", query)
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .stdin(Stdio::piped())
- .spawn()?;
-
- // Whether the request body is compressed.
- let gzip = matches!(
- headers.get("Content-Encoding").map(|h| h.to_str()),
- Some(Ok("gzip"))
- );
-
- {
- // This is safe because we captured the child's stdin.
- let mut stdin = child.stdin.take().unwrap();
-
- // Copy the request body to git-http-backend's stdin.
- if gzip {
- let mut decoder = GzDecoder::new(&mut stdin);
- let mut reader = body.reader();
-
- io::copy(&mut reader, &mut decoder)?;
- decoder.finish()?;
- } else {
- while body.has_remaining() {
- let mut chunk = body.chunk();
- let count = chunk.len();
-
- io::copy(&mut chunk, &mut stdin)?;
- body.advance(count);
- }
- }
- }
-
- match child.wait_with_output() {
- Ok(output) if output.status.success() => {
- tracing::info!("git-http-backend: exited successfully for {}", id);
-
- let mut reader = std::io::Cursor::new(output.stdout);
- let mut headers = HashMap::new();
-
- // Parse headers returned by git so that we can use them in the client response.
- for line in io::Read::by_ref(&mut reader).lines() {
- let line = line?;
-
- if line.is_empty() || line == "\r" {
- break;
- }
-
- let mut parts = line.splitn(2, ':');
- let key = parts.next();
- let value = parts.next();
-
- if let (Some(key), Some(value)) = (key, value) {
- let value = &value[1..];
-
- headers
- .entry(key.to_string())
- .or_insert_with(Vec::new)
- .push(value.to_string());
- } else {
- return Err(Error::BackendHeader(line));
- }
- }
-
- let status = {
- tracing::debug!("git-http-backend: {:?}", &headers);
-
- let line = headers.remove("Status").unwrap_or_default();
- let line = line.into_iter().next().unwrap_or_default();
- let mut parts = line.split(' ');
-
- parts
- .next()
- .and_then(|p| p.parse().ok())
- .unwrap_or(StatusCode::OK)
- };
-
- let position = reader.position() as usize;
- let body = reader.into_inner().split_off(position);
-
- Ok((status, headers, body))
- }
- Ok(output) => {
- if let Ok(output) = std::str::from_utf8(&output.stderr) {
- tracing::error!("git-http-backend: stderr: {}", output.trim_end());
- }
- Err(Error::BackendExited(output.status))
- }
- Err(err) => {
- panic!("failed to wait for git-http-backend: {err}");
- }
- }
-}
-
-#[cfg(test)]
-mod routes {
- use std::collections::HashMap;
- use std::net::SocketAddr;
- use std::str::FromStr;
-
- use axum::extract::connect_info::MockConnectInfo;
- use axum::http::StatusCode;
- use radicle::identity::RepoId;
-
- use crate::test::{self, get, RID};
-
- #[tokio::test]
- async fn test_info_request() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = test::seed(tmp.path());
- let app = super::router(ctx.profile().to_owned(), HashMap::new())
- .layer(MockConnectInfo(SocketAddr::from(([0, 0, 0, 0], 8080))));
-
- let response = get(&app, format!("/{RID}.git/info/refs")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- }
-
- #[tokio::test]
- async fn test_aliases() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = test::seed(tmp.path());
- let app = super::router(
- ctx.profile().to_owned(),
- HashMap::from_iter([(String::from("heartwood"), RepoId::from_str(RID).unwrap())]),
- )
- .layer(MockConnectInfo(SocketAddr::from(([0, 0, 0, 0], 8080))));
-
- let response = get(&app, "/woodheart.git/info/refs").await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
-
- let response = get(&app, "/heartwood.git/info/refs").await;
- assert_eq!(response.status(), StatusCode::OK);
- }
-}
diff --git a/radicle-httpd/src/lib.rs b/radicle-httpd/src/lib.rs
deleted file mode 100644
index 299e56f2..00000000
--- a/radicle-httpd/src/lib.rs
+++ /dev/null
@@ -1,180 +0,0 @@
-#![allow(clippy::type_complexity)]
-#![allow(clippy::too_many_arguments)]
-#![recursion_limit = "256"]
-pub mod commands;
-pub mod error;
-
-use std::collections::HashMap;
-use std::net::SocketAddr;
-use std::num::NonZeroUsize;
-use std::process::Command;
-use std::str;
-use std::sync::Arc;
-use std::time::Duration;
-
-use anyhow::Context as _;
-use axum::body::{Body, HttpBody};
-use axum::http::{Request, Response};
-use axum::middleware;
-use axum::Router;
-use tokio::net::TcpListener;
-use tower_http::trace::TraceLayer;
-use tracing::Span;
-
-use radicle::identity::RepoId;
-use radicle::Profile;
-
-use tracing_extra::{tracing_middleware, ColoredStatus, Paint, RequestId, TracingInfo};
-
-mod api;
-mod axum_extra;
-mod cache;
-mod git;
-mod raw;
-#[cfg(test)]
-mod test;
-mod tracing_extra;
-
-/// Default cache HTTP size.
-pub const DEFAULT_CACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(100) };
-
-#[derive(Debug, Clone)]
-pub struct Options {
- pub aliases: HashMap<String, RepoId>,
- pub listen: SocketAddr,
- pub cache: Option<NonZeroUsize>,
-}
-
-/// Run the Server.
-pub async fn run(options: Options) -> anyhow::Result<()> {
- let git_version = Command::new("git")
- .arg("version")
- .output()
- .context("'git' command must be available")?
- .stdout;
-
- tracing::info!("{}", str::from_utf8(&git_version)?.trim());
-
- let listener = TcpListener::bind(options.listen).await?;
-
- tracing::info!("listening on http://{}", options.listen);
-
- let profile = Profile::load()?;
- let request_id = RequestId::new();
-
- tracing::info!("using radicle home at {}", profile.home().path().display());
-
- let app =
- router(options, profile)?
- .layer(middleware::from_fn(tracing_middleware))
- .layer(
- TraceLayer::new_for_http()
- .make_span_with(move |_request: &Request<Body>| {
- tracing::info_span!("request", id = %request_id.clone().next())
- })
- .on_response(
- |response: &Response<Body>, latency: Duration, _span: &Span| {
- if let Some(info) = response.extensions().get::<TracingInfo>() {
- tracing::info!(
- "{} \"{} {} {:?}\" {} {:?} {}",
- info.connect_info.0,
- info.method,
- info.uri,
- info.version,
- ColoredStatus(response.status()),
- latency,
- Paint::dim(
- response
- .body()
- .size_hint()
- .exact()
- .map(|n| n.to_string())
- .unwrap_or("0".to_string())
- .into()
- ),
- );
- } else {
- tracing::info!("Processed");
- }
- },
- ),
- )
- .into_make_service_with_connect_info::<SocketAddr>();
-
- axum::serve(listener, app)
- .await
- .map_err(anyhow::Error::from)
-}
-
-/// Create a router consisting of other sub-routers.
-fn router(options: Options, profile: Profile) -> anyhow::Result<Router> {
- let profile = Arc::new(profile);
- let ctx = api::Context::new(profile.clone(), &options);
-
- let api_router = api::router(ctx);
- let git_router = git::router(profile.clone(), options.aliases);
- let raw_router = raw::router(profile);
-
- let app = Router::new()
- .merge(git_router)
- .nest("/api", api_router)
- .nest("/raw", raw_router);
-
- Ok(app)
-}
-
-pub mod logger {
- use tracing::dispatcher::Dispatch;
-
- pub fn init() -> Result<(), tracing::subscriber::SetGlobalDefaultError> {
- tracing::dispatcher::set_global_default(Dispatch::new(subscriber()))
- }
-
- #[cfg(feature = "logfmt")]
- pub fn subscriber() -> impl tracing::Subscriber {
- use tracing_subscriber::layer::SubscriberExt as _;
- use tracing_subscriber::EnvFilter;
-
- tracing_subscriber::Registry::default()
- .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
- .with(tracing_logfmt::layer())
- }
-
- #[cfg(not(feature = "logfmt"))]
- pub fn subscriber() -> impl tracing::Subscriber {
- tracing_subscriber::FmtSubscriber::builder()
- .with_target(false)
- .with_max_level(tracing::Level::DEBUG)
- .finish()
- }
-}
-
-#[cfg(test)]
-mod routes {
- use std::collections::HashMap;
- use std::net::SocketAddr;
-
- use axum::extract::connect_info::MockConnectInfo;
- use axum::http::StatusCode;
-
- use crate::test::{self, get};
-
- #[tokio::test]
- async fn test_invalid_route_returns_404() {
- let tmp = tempfile::tempdir().unwrap();
- let app = super::router(
- super::Options {
- aliases: HashMap::new(),
- listen: SocketAddr::from(([0, 0, 0, 0], 8080)),
- cache: None,
- },
- test::profile(tmp.path(), [0xff; 32]),
- )
- .unwrap()
- .layer(MockConnectInfo(SocketAddr::from(([0, 0, 0, 0], 8080))));
-
- let response = get(&app, "/aa/a").await;
-
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-}
diff --git a/radicle-httpd/src/main.rs b/radicle-httpd/src/main.rs
deleted file mode 100644
index bf4f9489..00000000
--- a/radicle-httpd/src/main.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use std::num::NonZeroUsize;
-use std::{collections::HashMap, process};
-
-use radicle::prelude::RepoId;
-use radicle_httpd as httpd;
-
-#[tokio::main]
-async fn main() -> anyhow::Result<()> {
- let options = parse_options()?;
-
- // SAFETY: The logger is only initialized once.
- httpd::logger::init().unwrap();
- tracing::info!("version {}-{}", env!("RADICLE_VERSION"), env!("GIT_HEAD"));
-
- match httpd::run(options).await {
- Ok(()) => {}
- Err(err) => {
- tracing::error!("Fatal: {:#}", err);
- process::exit(1);
- }
- }
- Ok(())
-}
-
-/// Parse command-line arguments into HTTP options.
-fn parse_options() -> Result<httpd::Options, lexopt::Error> {
- use lexopt::prelude::*;
-
- let mut parser = lexopt::Parser::from_env();
- let mut listen = None;
- let mut aliases = HashMap::new();
- let mut cache = Some(httpd::DEFAULT_CACHE_SIZE);
-
- while let Some(arg) = parser.next()? {
- match arg {
- Long("listen") => {
- let addr = parser.value()?.parse()?;
- listen = Some(addr);
- }
- Long("alias") | Short('a') => {
- let alias: String = parser.value()?.parse()?;
- let id: RepoId = parser.value()?.parse()?;
-
- aliases.insert(alias, id);
- }
- Long("cache") => {
- let size = parser.value()?.parse()?;
- cache = NonZeroUsize::new(size);
- }
- Long("help") | Short('h') => {
- println!("usage: radicle-httpd [--listen <addr>] [--alias <name> <rid>] [--cache <size>]..");
- process::exit(0);
- }
- _ => return Err(arg.unexpected()),
- }
- }
- Ok(httpd::Options {
- aliases,
- listen: listen.unwrap_or_else(|| ([0, 0, 0, 0], 8080).into()),
- cache,
- })
-}
diff --git a/radicle-httpd/src/raw.rs b/radicle-httpd/src/raw.rs
deleted file mode 100644
index cce1f337..00000000
--- a/radicle-httpd/src/raw.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-use std::sync::Arc;
-use std::time::Duration;
-
-use axum::extract::{Query, State};
-use axum::http::{header, HeaderValue, Method, 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;
-use radicle::storage::{ReadRepository, ReadStorage};
-use radicle_surf::{Oid, Repository};
-
-use crate::api::RawQuery;
-use crate::axum_extra::Path;
-use crate::error::RawError as Error;
-
-const MAX_BLOB_SIZE: usize = 4_194_304;
-
-static MIMES: &[(&str, &str)] = &[
- ("3gp", "video/3gpp"),
- ("7z", "application/x-7z-compressed"),
- ("aac", "audio/aac"),
- ("avi", "video/x-msvideo"),
- ("bin", "application/octet-stream"),
- ("bmp", "image/bmp"),
- ("bz", "application/x-bzip"),
- ("bz2", "application/x-bzip2"),
- ("csh", "application/x-csh"),
- ("css", "text/css"),
- ("csv", "text/csv"),
- ("doc", "application/msword"),
- (
- "docx",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- ),
- ("epub", "application/epub+zip"),
- ("gz", "application/gzip"),
- ("gif", "image/gif"),
- ("htm", "text/html"),
- ("html", "text/html"),
- ("ico", "image/vnd.microsoft.icon"),
- ("jar", "application/java-archive"),
- ("jpeg", "image/jpeg"),
- ("jpg", "image/jpeg"),
- ("js", "text/javascript"),
- ("json", "application/json"),
- ("mjs", "text/javascript"),
- ("mp3", "audio/mpeg"),
- ("mp4", "video/mp4"),
- ("mpeg", "video/mpeg"),
- ("odp", "application/vnd.oasis.opendocument.presentation"),
- ("ods", "application/vnd.oasis.opendocument.spreadsheet"),
- ("odt", "application/vnd.oasis.opendocument.text"),
- ("oga", "audio/ogg"),
- ("ogv", "video/ogg"),
- ("ogx", "application/ogg"),
- ("otf", "font/otf"),
- ("png", "image/png"),
- ("pdf", "application/pdf"),
- ("php", "application/x-httpd-php"),
- ("ppt", "application/vnd.ms-powerpoint"),
- (
- "pptx",
- "application/vnd.openxmlformats-officedocument.presentationml.presentation",
- ),
- ("rar", "application/vnd.rar"),
- ("rtf", "application/rtf"),
- ("sh", "application/x-sh"),
- ("svg", "image/svg+xml"),
- ("tar", "application/x-tar"),
- ("tif", "image/tiff"),
- ("tiff", "image/tiff"),
- ("ttf", "font/ttf"),
- ("txt", "text/plain"),
- ("wav", "audio/wav"),
- ("weba", "audio/webm"),
- ("webm", "video/webm"),
- ("webp", "image/webp"),
- ("woff", "font/woff"),
- ("woff2", "font/woff2"),
- ("xhtml", "application/xhtml+xml"),
- ("xls", "application/vnd.ms-excel"),
- (
- "xlsx",
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- ),
- ("xml", "application/xml"),
- ("zip", "application/zip"),
-];
-
-pub fn router(profile: Arc<Profile>) -> Router {
- Router::new()
- .route("/:rid/:sha/*path", get(file_by_commit_handler))
- .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(
- Path((rid, sha, path)): Path<(RepoId, Oid, String)>,
- State(profile): State<Arc<Profile>>,
-) -> impl IntoResponse {
- let storage = &profile.storage;
- let repo = storage.repository(rid)?;
-
- // Don't allow downloading raw files for private repos.
- if repo.identity_doc()?.visibility.is_private() {
- return Err(Error::NotFound);
- }
-
- let repo: Repository = repo.backend.into();
- let blob = repo.blob(sha, &path)?;
-
- blob_response(blob, path)
-}
-
-async fn file_by_canonical_head_handler(
- Path((rid, path)): Path<(RepoId, String)>,
- State(profile): State<Arc<Profile>>,
-) -> impl IntoResponse {
- let storage = &profile.storage;
- let repo = storage.repository(rid)?;
-
- // Don't allow downloading raw files for private repos.
- if repo.identity_doc()?.visibility.is_private() {
- return Err(Error::NotFound);
- }
-
- let (_, sha) = repo.head()?;
- let repo: Repository = repo.backend.into();
- let blob = repo.blob(sha, &path)?;
-
- blob_response(blob, path)
-}
-
-fn blob_response(
- blob: Blob<BlobRef>,
- path: String,
-) -> Result<(StatusCode, HeaderMap, Vec<u8>), Error> {
- let mut response_headers = HeaderMap::new();
- if blob.size() > MAX_BLOB_SIZE {
- return Ok::<_, Error>((StatusCode::PAYLOAD_TOO_LARGE, response_headers, vec![]));
- }
-
- let mime = if let Some(ext) = path.split('.').last() {
- MIMES
- .binary_search_by(|(k, _)| k.cmp(&ext))
- .map(|k| MIMES[k].1)
- .unwrap_or("text; charset=utf-8")
- } else {
- "application/octet-stream"
- };
- response_headers.insert(header::CONTENT_TYPE, HeaderValue::from_str(mime)?);
-
- Ok::<_, Error>((StatusCode::OK, response_headers, blob.content().to_owned()))
-}
-
-async fn file_by_oid_handler(
- Path((rid, oid)): Path<(RepoId, Oid)>,
- State(profile): State<Arc<Profile>>,
- Query(qs): Query<RawQuery>,
-) -> impl IntoResponse {
- let storage = &profile.storage;
- let repo = storage.repository(rid)?;
-
- // Don't allow downloading raw files for private repos.
- if repo.identity_doc()?.visibility.is_private() {
- return Err(Error::NotFound);
- }
-
- let blob = repo.blob(oid)?;
- let mut response_headers = HeaderMap::new();
-
- if blob.size() > MAX_BLOB_SIZE {
- return Ok::<_, Error>((StatusCode::PAYLOAD_TOO_LARGE, response_headers, vec![]));
- }
-
- response_headers.insert(
- header::CONTENT_TYPE,
- HeaderValue::from_str(&qs.mime.unwrap_or("application/octet-stream".to_string()))?,
- );
-
- Ok::<_, Error>((StatusCode::OK, response_headers, blob.content().to_vec()))
-}
-
-#[cfg(test)]
-mod routes {
- use axum::http::StatusCode;
-
- use crate::test::{self, get, RID, RID_PRIVATE};
- use radicle::storage::ReadStorage;
-
- #[tokio::test]
- async fn test_file_handler() {
- let tmp = tempfile::tempdir().unwrap();
- let ctx = test::seed(tmp.path());
- let app = super::router(ctx.profile().to_owned());
-
- let response = get(&app, format!("/{RID}/head/dir1/README")).await;
-
- assert_eq!(response.status(), StatusCode::OK);
- assert_eq!(response.body().await, "Hello World from dir1!\n");
-
- // Make sure the repo exists in storage.
- ctx.profile()
- .storage
- .repository(RID_PRIVATE.parse().unwrap())
- .unwrap();
-
- let response = get(&app, format!("/{RID_PRIVATE}/head/README")).await;
- assert_eq!(response.status(), StatusCode::NOT_FOUND);
- }
-}
diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs
deleted file mode 100644
index 47d0e032..00000000
--- a/radicle-httpd/src/test.rs
+++ /dev/null
@@ -1,395 +0,0 @@
-use std::collections::BTreeSet;
-use std::fs;
-use std::path::Path;
-use std::str::FromStr;
-use std::sync::Arc;
-
-use axum::body::{Body, Bytes};
-use axum::http::{Method, Request};
-use axum::Router;
-use serde_json::Value;
-use time::OffsetDateTime;
-use tower::ServiceExt;
-
-use radicle::cob::patch::MergeTarget;
-use radicle::crypto::ssh::keystore::MemorySigner;
-use radicle::crypto::ssh::Keystore;
-use radicle::crypto::{KeyPair, Seed, Signer};
-use radicle::git::{raw as git2, RefString};
-use radicle::identity::Visibility;
-use radicle::profile::{env, Home};
-use radicle::storage::ReadStorage;
-use radicle::Storage;
-use radicle::{node, profile};
-use radicle_crypto::test::signer::MockSigner;
-
-use crate::api::{auth, Context};
-
-pub const RID: &str = "rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp";
-pub const RID_PRIVATE: &str = "rad:zLuTzcmoWMcdK37xqArS8eckp9vK";
-pub const HEAD: &str = "e8c676b9e3b42308dc9d218b70faa5408f8e58ca";
-pub const PARENT: &str = "ee8d6a29304623a78ebfa5eeed5af674d0e58f83";
-pub const INITIAL_COMMIT: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
-pub const DID: &str = "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi";
-pub const ISSUE_ID: &str = "ca67d195c0b308b51810dedd93157a20764d5db5";
-pub const ISSUE_DISCUSSION_ID: &str = "41e2823caa54f1d53e375035ed4aabd0a89fa855";
-pub const ISSUE_COMMENT_ID: &str = "e9f963fab82ad875e46b29a327c5d3d51f825cdc";
-pub const SESSION_ID: &str = "u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy";
-pub const TIMESTAMP: u64 = 1671125284;
-pub const CONTRIBUTOR_RID: &str = "rad:z4XaCmN3jLSeiMvW15YTDpNbDHFhG";
-pub const CONTRIBUTOR_DID: &str = "did:key:z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8";
-pub const CONTRIBUTOR_ALIAS: &str = "seed";
-pub const CONTRIBUTOR_PATCH_ID: &str = "3e3f0dc34b3eeb64cfbc7218fbd52b97246e0564";
-
-/// Create a new profile.
-pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
- let home = Home::new(home).unwrap();
- let keystore = Keystore::new(&home.keys());
- let keypair = KeyPair::from_seed(Seed::from(seed));
- let alias = node::Alias::new("seed");
- let storage = Storage::open(
- home.storage(),
- radicle::git::UserInfo {
- alias: alias.clone(),
- key: keypair.pk.into(),
- },
- )
- .unwrap();
-
- let mut db = home.policies_mut().unwrap();
- db.follow(&keypair.pk.into(), Some(&alias)).unwrap();
-
- radicle::storage::git::transport::local::register(storage.clone());
- keystore.store(keypair.clone(), "radicle", None).unwrap();
-
- radicle::Profile {
- home,
- storage,
- keystore,
- public_key: keypair.pk.into(),
- config: profile::Config::new(alias),
- }
-}
-
-pub fn seed(dir: &Path) -> Context {
- let home = dir.join("radicle");
- let profile = profile(home.as_path(), [0xff; 32]);
- let signer = Box::new(MockSigner::from_seed([0xff; 32]));
-
- crate::logger::init().ok();
-
- seed_with_signer(dir, profile, &signer)
-}
-
-pub fn contributor(dir: &Path) -> Context {
- let mut seed = [0xff; 32];
- *seed.last_mut().unwrap() = 0xee;
-
- let home = dir.join("radicle");
- let profile = profile(home.as_path(), seed);
- let signer = MemorySigner::load(&profile.keystore, None).unwrap();
-
- seed_with_signer(dir, profile, &signer)
-}
-
-fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G) -> Context {
- const DEFAULT_BRANCH: &str = "master";
-
- crate::logger::init().ok();
-
- profile.policies_mut().unwrap();
- profile.database_mut().unwrap(); // Create the database.
-
- let mut policies = profile.policies_mut().unwrap();
- let workdir = dir.join("hello-world-private");
- fs::create_dir_all(&workdir).unwrap();
-
- // add commits to workdir (repo)
- let mut opts = git2::RepositoryInitOptions::new();
- opts.initial_head(DEFAULT_BRANCH);
- let repo = git2::Repository::init_opts(&workdir, &opts).unwrap();
- let tree = radicle::git::write_tree(
- Path::new("README"),
- "Hello Private World!\n".as_bytes(),
- &repo,
- )
- .unwrap();
-
- let sig_time = git2::Time::new(1673001014, 0);
- let sig = git2::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap();
-
- repo.commit(Some("HEAD"), &sig, &sig, "Initial commit\n", &tree, &[])
- .unwrap();
-
- // rad init
- let repo = git2::Repository::open(&workdir).unwrap();
- let name = "hello-world-private".to_string();
- let description = "Private Rad repository for tests".to_string();
- let branch = RefString::try_from(DEFAULT_BRANCH).unwrap();
- let visibility = Visibility::Private {
- allow: BTreeSet::default(),
- };
- let (rid, _, _) = radicle::rad::init(
- &repo,
- &name,
- &description,
- branch,
- visibility,
- signer,
- &profile.storage,
- )
- .unwrap();
-
- policies.seed(&rid, node::policy::Scope::All).unwrap();
-
- let workdir = dir.join("hello-world");
-
- env::set_var(env::GIT_COMMITTER_DATE, TIMESTAMP.to_string());
-
- fs::create_dir_all(&workdir).unwrap();
-
- // add commits to workdir (repo)
- let mut opts = git2::RepositoryInitOptions::new();
- opts.initial_head(DEFAULT_BRANCH);
- let repo = git2::Repository::init_opts(&workdir, &opts).unwrap();
- let tree =
- radicle::git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap();
-
- let sig_time = git2::Time::new(1673001014, 0);
- let sig = git2::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap();
-
- let oid = repo
- .commit(Some("HEAD"), &sig, &sig, "Initial commit\n", &tree, &[])
- .unwrap();
- let commit = repo.find_commit(oid).unwrap();
-
- repo.checkout_tree(tree.as_object(), None).unwrap();
-
- let tree = radicle::git::write_tree(
- Path::new("CONTRIBUTING"),
- "Thank you very much!\n".as_bytes(),
- &repo,
- )
- .unwrap();
- let sig_time = git2::Time::new(1673002014, 0);
- let sig = git2::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap();
-
- let oid2 = repo
- .commit(
- Some("HEAD"),
- &sig,
- &sig,
- "Add contributing file\n",
- &tree,
- &[&commit],
- )
- .unwrap();
- let commit2 = repo.find_commit(oid2).unwrap();
-
- repo.checkout_tree(tree.as_object(), None).unwrap();
-
- fs::create_dir(workdir.join("dir1")).unwrap();
- fs::write(
- workdir.join("dir1").join("README"),
- "Hello World from dir1!\n",
- )
- .unwrap();
- let mut index = repo.index().unwrap();
- index
- .add_all(["."], git2::IndexAddOption::DEFAULT, None)
- .unwrap();
- index.write().unwrap();
-
- let oid = index.write_tree().unwrap();
- let tree = repo.find_tree(oid).unwrap();
-
- let sig_time = git2::Time::new(1673003014, 0);
- let sig = git2::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap();
- repo.commit(
- Some("HEAD"),
- &sig,
- &sig,
- "Add another folder\n",
- &tree,
- &[&commit2],
- )
- .unwrap();
-
- // rad init
- let repo = git2::Repository::open(&workdir).unwrap();
- let name = "hello-world".to_string();
- let description = "Rad repository for tests".to_string();
- let branch = RefString::try_from(DEFAULT_BRANCH).unwrap();
- let visibility = Visibility::default();
- let (rid, _, _) = radicle::rad::init(
- &repo,
- &name,
- &description,
- branch,
- visibility,
- signer,
- &profile.storage,
- )
- .unwrap();
- policies.seed(&rid, node::policy::Scope::All).unwrap();
-
- let storage = &profile.storage;
- let repo = storage.repository(rid).unwrap();
- let mut issues = profile.issues_mut(&repo).unwrap();
- let issue = issues
- .create(
- "Issue #1".to_string(),
- "Change 'hello world' to 'hello everyone'".to_string(),
- &[],
- &[],
- [],
- signer,
- )
- .unwrap();
- tracing::debug!(target: "test", "Contributor issue: {}", issue.id());
-
- // eq. rad patch open
- let mut patches = profile.patches_mut(&repo).unwrap();
- let oid = radicle::git::Oid::from_str(HEAD).unwrap();
- let base = radicle::git::Oid::from_str(PARENT).unwrap();
- let patch = patches
- .create(
- "A new `hello world`",
- "change `hello world` in README to something else",
- MergeTarget::Delegates,
- base,
- oid,
- &[],
- signer,
- )
- .unwrap();
- tracing::debug!(target: "test", "Contributor patch: {}", patch.id());
-
- let options = crate::Options {
- aliases: std::collections::HashMap::new(),
- listen: std::net::SocketAddr::from(([0, 0, 0, 0], 8080)),
- cache: Some(crate::DEFAULT_CACHE_SIZE),
- };
-
- Context::new(Arc::new(profile), &options)
-}
-
-/// Adds an authorized session to the Context::sessions HashMap.
-pub async fn create_session(ctx: Context) {
- let issued_at = OffsetDateTime::now_utc();
- let mut sessions = ctx.sessions().write().await;
- sessions.insert(
- String::from(SESSION_ID),
- auth::Session {
- status: auth::AuthState::Authorized,
- public_key: ctx.profile().public_key,
- alias: ctx.profile().config.node.alias.clone(),
- issued_at,
- expires_at: issued_at
- .checked_add(auth::AUTHORIZED_SESSIONS_EXPIRATION)
- .unwrap(),
- },
- );
-}
-
-pub async fn get(app: &Router, path: impl ToString) -> Response {
- Response(
- app.clone()
- .oneshot(request(path, Method::GET, None, None))
- .await
- .unwrap(),
- )
-}
-
-pub async fn post(
- app: &Router,
- path: impl ToString,
- body: Option<Body>,
- auth: Option<String>,
-) -> Response {
- Response(
- app.clone()
- .oneshot(request(path, Method::POST, body, auth))
- .await
- .unwrap(),
- )
-}
-
-pub async fn patch(
- app: &Router,
- path: impl ToString,
- body: Option<Body>,
- auth: Option<String>,
-) -> Response {
- Response(
- app.clone()
- .oneshot(request(path, Method::PATCH, body, auth))
- .await
- .unwrap(),
- )
-}
-
-pub async fn put(
- app: &Router,
- path: impl ToString,
- body: Option<Body>,
- auth: Option<String>,
-) -> Response {
- Response(
- app.clone()
- .oneshot(request(path, Method::PUT, body, auth))
- .await
- .unwrap(),
- )
-}
-
-fn request(
- path: impl ToString,
- method: Method,
- body: Option<Body>,
- auth: Option<String>,
-) -> Request<Body> {
- let mut request = Request::builder()
- .method(method)
- .uri(path.to_string())
- .header("Content-Type", "application/json");
- if let Some(token) = auth {
- request = request.header("Authorization", format!("Bearer {token}"));
- }
-
- request.body(body.unwrap_or_else(Body::empty)).unwrap()
-}
-
-#[derive(Debug)]
-pub struct Response(axum::response::Response);
-
-impl Response {
- pub async fn json(self) -> Value {
- let body = self.body().await;
- serde_json::from_slice(&body).unwrap()
- }
-
- pub async fn id(self) -> radicle::git::Oid {
- let json = self.json().await;
- let string = json["id"].as_str().unwrap();
-
- radicle::git::Oid::from_str(string).unwrap()
- }
-
- pub async fn success(self) -> bool {
- let json = self.json().await;
- let success = json["success"].as_bool();
-
- success.unwrap_or(false)
- }
-
- pub fn status(&self) -> axum::http::StatusCode {
- self.0.status()
- }
-
- pub async fn body(self) -> Bytes {
- axum::body::to_bytes(self.0.into_body(), usize::MAX)
- .await
- .unwrap()
- }
-}
diff --git a/radicle-httpd/src/tracing_extra.rs b/radicle-httpd/src/tracing_extra.rs
deleted file mode 100644
index bb61321f..00000000
--- a/radicle-httpd/src/tracing_extra.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use std::fmt;
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::sync::Arc;
-
-use axum::body::Body;
-use axum::extract::ConnectInfo;
-use axum::http::Request;
-use axum::middleware::Next;
-use axum::response::IntoResponse;
-use axum::Extension;
-use hyper::{Method, StatusCode, Uri, Version};
-
-pub use radicle_term::ansi::Paint;
-
-#[derive(Clone)]
-pub struct RequestId(Arc<AtomicU64>);
-
-impl RequestId {
- pub fn new() -> RequestId {
- RequestId(Arc::new(0.into()))
- }
-
- pub fn next(&mut self) -> u64 {
- self.0.fetch_add(1, Ordering::SeqCst)
- }
-}
-
-#[derive(Clone)]
-pub struct TracingInfo {
- pub connect_info: ConnectInfo<SocketAddr>,
- pub method: Method,
- pub version: Version,
- pub uri: Uri,
-}
-
-pub struct ColoredStatus(pub StatusCode);
-
-impl fmt::Display for ColoredStatus {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self.0.as_u16() {
- 200..=299 => write!(f, "{}", Paint::green(self.0)),
- 300..=399 => write!(f, "{}", Paint::blue(self.0)),
- 400..=499 => write!(f, "{}", Paint::red(self.0)),
- _ => write!(f, "{}", Paint::yellow(self.0)),
- }
- }
-}
-
-pub async fn tracing_middleware(request: Request<Body>, next: Next) -> impl IntoResponse {
- let connect_info = *request
- .extensions()
- .get::<ConnectInfo<std::net::SocketAddr>>()
- .unwrap();
-
- let method = request.method().clone();
- let version = request.version();
- let uri = request.uri().clone();
-
- let tracing_info = TracingInfo {
- connect_info,
- method,
- version,
- uri,
- };
-
- let response = next.run(request).await;
-
- (Extension(tracing_info), response)
-}
diff --git a/systemd/radicle-httpd.service b/systemd/radicle-httpd.service
deleted file mode 100644
index d6266207..00000000
--- a/systemd/radicle-httpd.service
+++ /dev/null
@@ -1,23 +0,0 @@
-# Example systemd unit file for `radicle-httpd`.
-#
-# When running radicle-httpd on a server, it should be run as a separate user.
-#
-# Copy this file into /etc/systemd/system and set the User/Group parameters
-# under [Service] appropriately, as well as the `RAD_HOME` environment variable.
-#
-[Unit]
-Description=Radicle HTTP Daemon
-After=network.target network-online.target
-Requires=network-online.target
-
-[Service]
-User=seed
-Group=seed
-ExecStart=/usr/local/bin/radicle-httpd --listen 127.0.0.1:8080
-Environment=RAD_HOME=/home/seed/.radicle RUST_BACKTRACE=1 RUST_LOG=info
-KillMode=process
-Restart=always
-RestartSec=1
-
-[Install]
-WantedBy=multi-user.target
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name e9a02dd5-6255-49c5-a910-3b567390b8bc -v /opt/radcis/ci.rad.levitte.org/cci/state/e9a02dd5-6255-49c5-a910-3b567390b8bc/s:/e9a02dd5-6255-49c5-a910-3b567390b8bc/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/e9a02dd5-6255-49c5-a910-3b567390b8bc/w:/e9a02dd5-6255-49c5-a910-3b567390b8bc/w -w /e9a02dd5-6255-49c5-a910-3b567390b8bc/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /e9a02dd5-6255-49c5-a910-3b567390b8bc/s/script.sh
+ cargo --version
info: syncing channel updates for '1.77-x86_64-unknown-linux-gnu'
info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.77.2 (e52e36006 2024-03-26)
+ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.77-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.77-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}