Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Remove `radicle-httpd` crate
Merged did:key:z6MksFqX...wzpT opened 1 year ago

The HTTP daemon is moved to the radicle-explorer repository. Hence, all traces of it are removed from this repository.

34 files changed +33 -7910 ab532be0 a4989b7c
modified Cargo.lock
@@ -3,15 +3,6 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -53,18 +44,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -74,12 +53,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -205,122 +178,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
]

@@ -449,12 +312,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -705,7 +562,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
 "powerfmt",
-
 "serde",
]

[[package]]
@@ -876,12 +732,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -891,45 +741,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -971,12 +782,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1545,39 +1350,10 @@ dependencies = [
]

[[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"
@@ -1598,89 +1374,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1889,30 +1582,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1939,12 +1608,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1954,17 +1617,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2031,16 +1683,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2094,16 +1736,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2119,15 +1751,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2140,12 +1763,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2232,38 +1849,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -2630,25 +2123,6 @@ dependencies = [
]

[[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"
dependencies = [
@@ -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",
@@ -2684,38 +2158,8 @@ dependencies = [
]

[[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",
]
@@ -2755,43 +2199,6 @@ dependencies = [
]

[[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"
dependencies = [
@@ -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",
]
@@ -2851,30 +2258,8 @@ dependencies = [
]

[[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",
@@ -2931,36 +2315,14 @@ dependencies = [
]

[[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,17 +2390,11 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
@@ -3084,12 +2431,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3103,12 +2444,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3196,28 +2531,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3245,15 +2558,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3300,15 +2604,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3480,18 +2775,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3559,16 +2842,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3623,154 +2896,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3912,12 +3037,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3976,20 +3095,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3998,7 +3103,6 @@ dependencies = [
 "form_urlencoded",
 "idna",
 "percent-encoding",
-
 "serde",
]

[[package]]
@@ -4008,12 +3112,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4036,15 +3134,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4105,22 +3194,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4130,12 +3203,6 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4319,26 +3386,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified Cargo.toml
@@ -9,7 +9,6 @@ members = [
  "radicle-crypto",
  "radicle-dag",
  "radicle-fetch",
-
  "radicle-httpd",
  "radicle-node",
  "radicle-remote-helper",
  "radicle-ssh",
modified 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

modified 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).

modified 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/
modified 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:
modified flake.nix
@@ -171,10 +171,6 @@
          ({name, ...} @ package: lib.nameValuePair name (crate package))
          [
            {
-
              name = "radicle-httpd";
-
              pages = ["radicle-httpd.1.adoc"];
-
            }
-
            {
              name = "radicle-cli";
              pages = [
                "rad.1.adoc"
@@ -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; [
deleted radicle-httpd.1.adoc
@@ -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)
deleted radicle-httpd/Cargo.toml
@@ -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"] }
deleted radicle-httpd/build.rs
@@ -1 +0,0 @@
-
../build.rs

\ No newline at end of file
deleted radicle-httpd/src/api.rs
@@ -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()),
-
    }
-
}
deleted radicle-httpd/src/api/auth.rs
@@ -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(())
-
}
deleted radicle-httpd/src/api/error.rs
@@ -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()
-
    }
-
}
deleted radicle-httpd/src/api/json.rs
@@ -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)
-
}
deleted radicle-httpd/src/api/v1.rs
@@ -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)
-
}
deleted radicle-httpd/src/api/v1/delegates.rs
@@ -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,
-
              }
-
            ])
-
        );
-
    }
-
}
deleted radicle-httpd/src/api/v1/node.rs
@@ -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 }))))
-
}
deleted radicle-httpd/src/api/v1/profile.rs
@@ -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()
-
            })
-
        );
-
    }
-
}
deleted radicle-httpd/src/api/v1/projects.rs
@@ -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);
-
    }
-
}
deleted radicle-httpd/src/api/v1/sessions.rs
@@ -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);
-
    }
-
}
deleted radicle-httpd/src/api/v1/stats.rs
@@ -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 } }));
-
    }
-
}
deleted radicle-httpd/src/axum_extra.rs
@@ -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),
-
    )
-
}
deleted radicle-httpd/src/bin/rad-web.rs
@@ -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(),
-
    )
-
}
deleted radicle-httpd/src/cache.rs
@@ -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))),
-
        }
-
    }
-
}
deleted radicle-httpd/src/commands.rs
@@ -1,2 +0,0 @@
-
//! Extra CLI commands relating to HTTPd.
-
pub mod web;
deleted radicle-httpd/src/commands/web.rs
@@ -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(())
-
}
deleted radicle-httpd/src/error.rs
@@ -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()
-
    }
-
}
deleted radicle-httpd/src/git.rs
@@ -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);
-
    }
-
}
deleted radicle-httpd/src/lib.rs
@@ -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);
-
    }
-
}
deleted radicle-httpd/src/main.rs
@@ -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,
-
    })
-
}
deleted radicle-httpd/src/raw.rs
@@ -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);
-
    }
-
}
deleted radicle-httpd/src/test.rs
@@ -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()
-
    }
-
}
deleted radicle-httpd/src/tracing_extra.rs
@@ -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)
-
}
deleted systemd/radicle-httpd.service
@@ -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