httpd: Dogfood the new radicle-surf API
Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
parent
f33e79f14b
commit
4ea67250ea
|
|
@ -1067,7 +1067,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git-ref-format"
|
name = "git-ref-format"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"git-ref-format-core",
|
"git-ref-format-core",
|
||||||
"git-ref-format-macro",
|
"git-ref-format-macro",
|
||||||
|
|
@ -1076,7 +1076,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git-ref-format-core"
|
name = "git-ref-format-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
@ -1085,7 +1085,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git-ref-format-macro"
|
name = "git-ref-format-macro"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"git-ref-format-core",
|
"git-ref-format-core",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
|
|
@ -2229,7 +2229,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radicle-git-ext"
|
name = "radicle-git-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"git-ref-format",
|
"git-ref-format",
|
||||||
"git2",
|
"git2",
|
||||||
|
|
@ -2325,12 +2325,12 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radicle-std-ext"
|
name = "radicle-std-ext"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radicle-surf"
|
name = "radicle-surf"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ version = "0.1.0"
|
||||||
|
|
||||||
[patch.crates-io.radicle-git-ext]
|
[patch.crates-io.radicle-git-ext]
|
||||||
git = "https://github.com/radicle-dev/radicle-git"
|
git = "https://github.com/radicle-dev/radicle-git"
|
||||||
rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
rev = "016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
|
|
||||||
[patch.crates-io.git-ref-format]
|
[patch.crates-io.git-ref-format]
|
||||||
git = "https://github.com/radicle-dev/radicle-git"
|
git = "https://github.com/radicle-dev/radicle-git"
|
||||||
rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
rev = "016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ version = "0.2.0"
|
||||||
[dependencies.radicle-surf]
|
[dependencies.radicle-surf]
|
||||||
git = "https://github.com/radicle-dev/radicle-git"
|
git = "https://github.com/radicle-dev/radicle-git"
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09"
|
rev = "016ece66474edf45d2d304141515152e6f7b37a3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hyper = { version = "0.14.17", default-features = false, features = ["client"] }
|
hyper = { version = "0.14.17", default-features = false, features = ["client"] }
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use radicle::Profile;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod axum_extra;
|
mod axum_extra;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod json;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
mod v1;
|
mod v1;
|
||||||
|
|
|
||||||
|
|
@ -38,17 +38,13 @@ pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
GitProject(#[from] radicle::storage::git::ProjectError),
|
GitProject(#[from] radicle::storage::git::ProjectError),
|
||||||
|
|
||||||
/// Surf commit error.
|
/// Surf directory error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SurfCommit(#[from] radicle_surf::source::commit::Error),
|
SurfDir(#[from] radicle_surf::fs::error::Directory),
|
||||||
|
|
||||||
/// Surf object error.
|
/// Surf error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SurfObject(#[from] radicle_surf::source::object::Error),
|
Surf(#[from] radicle_surf::Error),
|
||||||
|
|
||||||
/// Surf git error.
|
|
||||||
#[error(transparent)]
|
|
||||||
SurfGit(#[from] radicle_surf::git::Error),
|
|
||||||
|
|
||||||
/// Git2 error.
|
/// Git2 error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
//! Utilities for building JSON responses of our API.
|
||||||
|
|
||||||
|
use radicle_surf::{
|
||||||
|
object::{Blob, Tree},
|
||||||
|
Commit, Stats,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
/// Returns JSON of a commit.
|
||||||
|
pub(crate) fn commit(commit: &Commit) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"sha1": commit.id,
|
||||||
|
"author": {
|
||||||
|
"name": commit.author.name,
|
||||||
|
"email": commit.author.email
|
||||||
|
},
|
||||||
|
"summary": commit.summary,
|
||||||
|
"description": commit.description(),
|
||||||
|
"committer": {
|
||||||
|
"name": commit.committer.name,
|
||||||
|
"email": commit.committer.email
|
||||||
|
},
|
||||||
|
"committerTime": commit.committer.time.seconds()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns JSON for a blob with a given `path`.
|
||||||
|
pub(crate) fn blob(blob: &Blob, path: &str) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"binary": blob.is_binary(),
|
||||||
|
"content": blob.content(),
|
||||||
|
"name": name_in_path(path),
|
||||||
|
"path": path,
|
||||||
|
"lastCommit": commit(blob.commit())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns JSON for a tree with a given `path` and `stats`.
|
||||||
|
pub(crate) fn tree(tree: &Tree, path: &str, stats: &Stats) -> serde_json::Value {
|
||||||
|
let prefix = std::path::Path::new(path);
|
||||||
|
let entries = tree
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
json!({
|
||||||
|
"path": prefix.join(entry.name()),
|
||||||
|
"name": entry.name(),
|
||||||
|
"lastCommit": serde_json::Value::Null,
|
||||||
|
"kind": if entry.is_tree() { "tree" } else { "blob" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
json!({
|
||||||
|
"entries": &entries,
|
||||||
|
"lastCommit": commit(tree.commit()),
|
||||||
|
"name": name_in_path(path),
|
||||||
|
"path": path,
|
||||||
|
"stats": stats,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the name part of a path string.
|
||||||
|
fn name_in_path(path: &str) -> &str {
|
||||||
|
match path.rsplit('/').next() {
|
||||||
|
Some(name) => name,
|
||||||
|
None => path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use axum::handler::Handler;
|
use axum::handler::Handler;
|
||||||
use axum::http::{header, HeaderValue};
|
use axum::http::{header, HeaderValue};
|
||||||
|
|
@ -13,20 +13,15 @@ use tower_http::set_header::SetResponseHeaderLayer;
|
||||||
use radicle::cob::issue::Issues;
|
use radicle::cob::issue::Issues;
|
||||||
use radicle::cob::thread::{self, CommentId};
|
use radicle::cob::thread::{self, CommentId};
|
||||||
use radicle::cob::Timestamp;
|
use radicle::cob::Timestamp;
|
||||||
use radicle::git::Oid;
|
|
||||||
use radicle::identity::{Id, PublicKey};
|
use radicle::identity::{Id, PublicKey};
|
||||||
use radicle::node::NodeId;
|
use radicle::node::NodeId;
|
||||||
use radicle::storage::git::paths;
|
use radicle::storage::{git::paths, ReadRepository, WriteStorage};
|
||||||
use radicle::storage::{ReadRepository, WriteStorage};
|
use radicle_surf::{Glob, Oid, Repository};
|
||||||
use radicle_surf::git::{Glob, History, Repository};
|
|
||||||
use radicle_surf::source;
|
|
||||||
use radicle_surf::source::object::blob::Blob;
|
|
||||||
use radicle_surf::source::object::tree::Tree;
|
|
||||||
|
|
||||||
use crate::api::axum_extra::{Path, Query};
|
use crate::api::axum_extra::{Path, Query};
|
||||||
use crate::api::error::Error;
|
use crate::api::error::Error;
|
||||||
use crate::api::project::Info;
|
use crate::api::project::Info;
|
||||||
use crate::api::{Context, PaginationQuery};
|
use crate::api::{self, Context, PaginationQuery};
|
||||||
|
|
||||||
const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";
|
const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";
|
||||||
|
|
||||||
|
|
@ -147,7 +142,7 @@ async fn history_handler(
|
||||||
};
|
};
|
||||||
|
|
||||||
let headers = repo
|
let headers = repo
|
||||||
.history(sha)?
|
.history(&sha)?
|
||||||
.filter(|q| {
|
.filter(|q| {
|
||||||
if let Ok(q) = q {
|
if let Ok(q) = q {
|
||||||
if let (Some(since), Some(until)) = (since, until) {
|
if let (Some(since), Some(until)) = (since, until) {
|
||||||
|
|
@ -166,14 +161,23 @@ async fn history_handler(
|
||||||
})
|
})
|
||||||
.skip(page * per_page)
|
.skip(page * per_page)
|
||||||
.take(per_page)
|
.take(per_page)
|
||||||
.filter_map(|commit| {
|
.map(|r| {
|
||||||
if let Ok(commit) = commit {
|
r.and_then(|c| {
|
||||||
source::commit(&repo, commit.id).ok()
|
let glob = Glob::all_heads().branches().and(Glob::all_remotes());
|
||||||
} else {
|
let branches: Vec<String> = repo
|
||||||
None
|
.revision_branches(c.id, glob)?
|
||||||
}
|
.iter()
|
||||||
|
.map(|b| b.refname().to_string())
|
||||||
|
.collect();
|
||||||
|
let diff = repo.diff_commit(c.id)?;
|
||||||
|
Ok(json!({
|
||||||
|
"header": api::json::commit(&c),
|
||||||
|
"diff": diff,
|
||||||
|
"branches": branches
|
||||||
|
}))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
let response = json!({
|
let response = json!({
|
||||||
"headers": headers,
|
"headers": headers,
|
||||||
|
|
@ -195,9 +199,22 @@ async fn commit_handler(
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let storage = &ctx.profile.storage;
|
let storage = &ctx.profile.storage;
|
||||||
let repo = Repository::open(paths::repository(storage, &project))?;
|
let repo = Repository::open(paths::repository(storage, &project))?;
|
||||||
let commit = source::commit(&repo, sha)?;
|
let commit = repo.commit(sha)?;
|
||||||
|
|
||||||
Ok::<_, Error>(Json(commit))
|
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 response = json!({
|
||||||
|
"header": api::json::commit(&commit),
|
||||||
|
"diff": diff,
|
||||||
|
"branches": branches
|
||||||
|
});
|
||||||
|
Ok::<_, Error>(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get project activity for the past year.
|
/// Get project activity for the past year.
|
||||||
|
|
@ -236,10 +253,9 @@ async fn tree_handler(
|
||||||
let path = path.strip_prefix('/').ok_or(Error::NotFound)?.to_string();
|
let path = path.strip_prefix('/').ok_or(Error::NotFound)?.to_string();
|
||||||
let storage = &ctx.profile.storage;
|
let storage = &ctx.profile.storage;
|
||||||
let repo = Repository::open(paths::repository(storage, &project))?;
|
let repo = Repository::open(paths::repository(storage, &project))?;
|
||||||
let path = if path.is_empty() { None } else { Some(&path) };
|
let tree = repo.tree(sha, &path)?;
|
||||||
let tree = Tree::new(&repo, &sha, path)?;
|
let stats = repo.stats_from(&sha)?;
|
||||||
let mut response = json!(tree);
|
let response = api::json::tree(&tree, &path, &stats);
|
||||||
response["stats"] = json!(stats(&repo, sha)?);
|
|
||||||
|
|
||||||
Ok::<_, Error>(Json(response))
|
Ok::<_, Error>(Json(response))
|
||||||
}
|
}
|
||||||
|
|
@ -313,9 +329,10 @@ async fn blob_handler(
|
||||||
let path = path.strip_prefix('/').ok_or(Error::NotFound)?;
|
let path = path.strip_prefix('/').ok_or(Error::NotFound)?;
|
||||||
let storage = &ctx.profile.storage;
|
let storage = &ctx.profile.storage;
|
||||||
let repo = Repository::open(paths::repository(storage, &project))?;
|
let repo = Repository::open(paths::repository(storage, &project))?;
|
||||||
let blob = Blob::new(&repo, &sha, &path)?;
|
let blob = repo.blob(sha, &path)?;
|
||||||
|
let response = api::json::blob(&blob, path);
|
||||||
|
|
||||||
Ok::<_, Error>(Json(blob))
|
Ok::<_, Error>(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get project readme.
|
/// Get project readme.
|
||||||
|
|
@ -336,8 +353,9 @@ async fn readme_handler(
|
||||||
];
|
];
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if let Ok(blob) = Blob::new(&repo, &sha, &path) {
|
if let Ok(blob) = repo.blob(sha, path) {
|
||||||
return Ok::<_, Error>(Json(blob));
|
let response = api::json::blob(&blob, path);
|
||||||
|
return Ok::<_, Error>(Json(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,35 +457,6 @@ impl<'a> FromIterator<(&'a CommentId, &'a thread::Comment)> for Comments {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Stats {
|
|
||||||
branches: usize,
|
|
||||||
commits: usize,
|
|
||||||
contributors: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stats(repo: &Repository, head: Oid) -> Result<Stats, Error> {
|
|
||||||
let branches = repo.branches(Glob::all_heads())?.count();
|
|
||||||
|
|
||||||
let mut commits = 0;
|
|
||||||
let contributors = History::new(repo, head)?
|
|
||||||
.filter_map(|commit| {
|
|
||||||
commits += 1;
|
|
||||||
if let Ok(commit) = commit {
|
|
||||||
Some((commit.author.name, commit.author.email))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
|
|
||||||
Ok(Stats {
|
|
||||||
branches,
|
|
||||||
commits,
|
|
||||||
contributors: contributors.len(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod routes {
|
mod routes {
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
|
@ -722,17 +711,17 @@ mod routes {
|
||||||
response.json().await,
|
response.json().await,
|
||||||
json!({
|
json!({
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
|
||||||
"path": "README",
|
|
||||||
"name": "README",
|
|
||||||
"lastCommit": null,
|
|
||||||
"kind": "blob"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "dir1",
|
"path": "dir1",
|
||||||
"name": "dir1",
|
"name": "dir1",
|
||||||
"lastCommit": null,
|
"lastCommit": null,
|
||||||
"kind": "tree"
|
"kind": "tree"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README",
|
||||||
|
"name": "README",
|
||||||
|
"lastCommit": null,
|
||||||
|
"kind": "blob"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastCommit": {
|
"lastCommit": {
|
||||||
|
|
@ -778,7 +767,20 @@ mod routes {
|
||||||
"kind": "blob"
|
"kind": "blob"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastCommit": null,
|
"lastCommit": {
|
||||||
|
"sha1": HEAD,
|
||||||
|
"author": {
|
||||||
|
"name": "Alice Liddell",
|
||||||
|
"email": "alice@radicle.xyz"
|
||||||
|
},
|
||||||
|
"summary": "Add another folder",
|
||||||
|
"description": "",
|
||||||
|
"committer": {
|
||||||
|
"name": "Alice Liddell",
|
||||||
|
"email": "alice@radicle.xyz"
|
||||||
|
},
|
||||||
|
"committerTime": 1673001014
|
||||||
|
},
|
||||||
"name": "dir1",
|
"name": "dir1",
|
||||||
"path": "dir1",
|
"path": "dir1",
|
||||||
"stats": {
|
"stats": {
|
||||||
|
|
@ -866,6 +868,42 @@ mod routes {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_projects_readme() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let app = super::router(test::seed(tmp.path()));
|
||||||
|
let response = request(
|
||||||
|
&app,
|
||||||
|
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/readme/{HEAD}"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
response.json().await,
|
||||||
|
json!({
|
||||||
|
"binary": false,
|
||||||
|
"content": "Hello World!\n",
|
||||||
|
"lastCommit": {
|
||||||
|
"sha1": HEAD_1,
|
||||||
|
"author": {
|
||||||
|
"name": "Alice Liddell",
|
||||||
|
"email": "alice@radicle.xyz"
|
||||||
|
},
|
||||||
|
"summary": "Initial commit",
|
||||||
|
"description": "",
|
||||||
|
"committer": {
|
||||||
|
"name": "Alice Liddell",
|
||||||
|
"email": "alice@radicle.xyz"
|
||||||
|
},
|
||||||
|
"committerTime": 1673001014
|
||||||
|
},
|
||||||
|
"name": "README",
|
||||||
|
"path": "README"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_projects_issues_root() {
|
async fn test_projects_issues_root() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue