httpd: Add `alias` field to certain routes

* `/patches`, `/issues`, `/remotes`

Signed-off-by: xphoniex <dj.2dixx@gmail.com>
This commit is contained in:
xphoniex 2023-05-10 16:32:21 +00:00 committed by Alexis Sellier
parent d8711a8d43
commit ae5982cd6b
No known key found for this signature in database
5 changed files with 79 additions and 18 deletions

View File

@ -69,6 +69,10 @@ pub enum Error {
/// Identity doc error. /// Identity doc error.
#[error(transparent)] #[error(transparent)]
IdentityDoc(#[from] radicle::identity::doc::DocError), IdentityDoc(#[from] radicle::identity::doc::DocError),
/// Tracking store error.
#[error(transparent)]
TrackingStore(#[from] radicle::node::tracking::store::Error),
} }
impl IntoResponse for Error { impl IntoResponse for Error {

View File

@ -12,6 +12,7 @@ use radicle::cob::thread;
use radicle::cob::thread::{CommentId, Thread}; use radicle::cob::thread::{CommentId, Thread};
use radicle::cob::{ActorId, Author, Reaction, Timestamp}; use radicle::cob::{ActorId, Author, Reaction, Timestamp};
use radicle::git::RefString; use radicle::git::RefString;
use radicle::node::tracking::store as TrackingStore;
use radicle::storage::{git, refs, ReadRepository}; use radicle::storage::{git, refs, ReadRepository};
use radicle_surf::blob::Blob; use radicle_surf::blob::Blob;
use radicle_surf::tree::Tree; use radicle_surf::tree::Tree;
@ -92,26 +93,31 @@ pub(crate) fn tree(tree: &Tree, path: &str, stats: &Stats) -> Value {
} }
/// Returns JSON for an `issue`. /// Returns JSON for an `issue`.
pub(crate) fn issue(id: IssueId, issue: Issue) -> Value { pub(crate) fn issue(id: IssueId, issue: Issue, aliases: &TrackingStore::Config) -> Value {
json!({ json!({
"id": id.to_string(), "id": id.to_string(),
"author": issue.author(), "author": author(&issue.author(), aliases.alias(issue.author().id())),
"title": issue.title(), "title": issue.title(),
"state": issue.state(), "state": issue.state(),
"assignees": issue.assigned().collect::<Vec<_>>(), "assignees": issue.assigned().collect::<Vec<_>>(),
"discussion": issue "discussion": issue
.comments() .comments()
.map(|(id, comment)| Comment::new(id, comment, issue.thread())) .map(|(id, comment)| Comment::new(id, comment, issue.thread(), aliases))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
"tags": issue.tags().collect::<Vec<_>>(), "tags": issue.tags().collect::<Vec<_>>(),
}) })
} }
/// Returns JSON for a `patch`. /// Returns JSON for a `patch`.
pub(crate) fn patch(id: PatchId, patch: Patch, repo: &git::Repository) -> Value { pub(crate) fn patch(
id: PatchId,
patch: Patch,
repo: &git::Repository,
aliases: &TrackingStore::Config,
) -> Value {
json!({ json!({
"id": id.to_string(), "id": id.to_string(),
"author": patch.author(), "author": author(patch.author(), aliases.alias(patch.author().id())),
"title": patch.title(), "title": patch.title(),
"description": patch.description(), "description": patch.description(),
"state": patch.state(), "state": patch.state(),
@ -126,7 +132,7 @@ pub(crate) fn patch(id: PatchId, patch: Patch, repo: &git::Repository) -> Value
"refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or(vec![]), "refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or(vec![]),
"merges": rev.merges().collect::<Vec<_>>(), "merges": rev.merges().collect::<Vec<_>>(),
"discussions": rev.discussion().comments() "discussions": rev.discussion().comments()
.map(|(id, comment)| Comment::new(id, comment, rev.discussion())) .map(|(id, comment)| Comment::new(id, comment, rev.discussion(), aliases))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
"timestamp": rev.timestamp(), "timestamp": rev.timestamp(),
"reviews": rev.reviews().collect::<Vec<_>>(), "reviews": rev.reviews().collect::<Vec<_>>(),
@ -135,6 +141,17 @@ pub(crate) fn patch(id: PatchId, patch: Patch, repo: &git::Repository) -> Value
}) })
} }
/// Returns JSON for an `author` and fills in `alias` when present.
fn author(author: &Author, alias: Option<String>) -> Value {
match alias {
Some(alias) => json!({
"id": author.id,
"alias": alias,
}),
None => json!(author),
}
}
/// Returns the name part of a path string. /// Returns the name part of a path string.
fn name_in_path(path: &str) -> &str { fn name_in_path(path: &str) -> &str {
match path.rsplit('/').next() { match path.rsplit('/').next() {
@ -168,7 +185,7 @@ fn get_refs(
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Comment<'a> { struct Comment<'a> {
id: CommentId, id: CommentId,
author: Author, author: Value,
body: &'a str, body: &'a str,
reactions: Vec<(&'a ActorId, &'a Reaction)>, reactions: Vec<(&'a ActorId, &'a Reaction)>,
timestamp: Timestamp, timestamp: Timestamp,
@ -176,10 +193,16 @@ struct Comment<'a> {
} }
impl<'a> Comment<'a> { impl<'a> Comment<'a> {
fn new(id: &'a CommentId, comment: &'a thread::Comment, thread: &'a Thread) -> Self { fn new(
id: &'a CommentId,
comment: &'a thread::Comment,
thread: &'a Thread,
aliases: &TrackingStore::Config,
) -> Self {
let comment_author = Author::new(comment.author());
Self { Self {
id: *id, id: *id,
author: Author::new(comment.author()), author: author(&comment_author, aliases.alias(comment_author.id())),
body: comment.body(), body: comment.body(),
reactions: thread.reactions(id).collect::<Vec<_>>(), reactions: thread.reactions(id).collect::<Vec<_>>(),
timestamp: comment.timestamp(), timestamp: comment.timestamp(),

View File

@ -319,6 +319,7 @@ async fn remotes_handler(State(ctx): State<Context>, Path(project): Path<Id>) ->
let storage = &ctx.profile.storage; let storage = &ctx.profile.storage;
let repo = storage.repository(project)?; let repo = storage.repository(project)?;
let delegates = repo.delegates()?; let delegates = repo.delegates()?;
let tracking_store = &ctx.profile.tracking()?;
let remotes = repo let remotes = repo
.remotes()? .remotes()?
.filter_map(|r| r.map(|r| r.1).ok()) .filter_map(|r| r.map(|r| r.1).ok())
@ -333,11 +334,19 @@ async fn remotes_handler(State(ctx): State<Context>, Path(project): Path<Id>) ->
}) })
.collect::<BTreeMap<String, &Oid>>(); .collect::<BTreeMap<String, &Oid>>();
json!({ match tracking_store.alias(&remote.id) {
"id": remote.id, Some(alias) => json!({
"heads": refs, "id": remote.id,
"delegate": delegates.contains(&remote.id.into()), "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<_>>(); .collect::<Vec<_>>();
@ -440,9 +449,10 @@ async fn issues_handler(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
issues.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp())); issues.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp()));
let tracking_store = &ctx.profile.tracking()?;
let issues = issues let issues = issues
.into_iter() .into_iter()
.map(|(id, issue, _)| api::json::issue(id, issue)) .map(|(id, issue, _)| api::json::issue(id, issue, tracking_store))
.skip(page * per_page) .skip(page * per_page)
.take(per_page) .take(per_page)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -559,8 +569,13 @@ async fn issue_handler(
let issue = issue::Issues::open(&repo)? let issue = issue::Issues::open(&repo)?
.get(&issue_id.into())? .get(&issue_id.into())?
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let tracking_store = &ctx.profile.tracking()?;
Ok::<_, Error>(Json(api::json::issue(issue_id.into(), issue))) Ok::<_, Error>(Json(api::json::issue(
issue_id.into(),
issue,
tracking_store,
)))
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -722,9 +737,10 @@ async fn patches_handler(
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
patches.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp())); patches.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp()));
let tracking_store = &ctx.profile.tracking()?;
let patches = patches let patches = patches
.into_iter() .into_iter()
.map(|(id, patch, _)| api::json::patch(id, patch, &repo)) .map(|(id, patch, _)| api::json::patch(id, patch, &repo, tracking_store))
.skip(page * per_page) .skip(page * per_page)
.take(per_page) .take(per_page)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -743,8 +759,14 @@ async fn patch_handler(
let patch = patch::Patches::open(&repo)? let patch = patch::Patches::open(&repo)?
.get(&patch_id.into())? .get(&patch_id.into())?
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let tracking_store = &ctx.profile.tracking()?;
Ok::<_, Error>(Json(api::json::patch(patch_id.into(), patch, &repo))) Ok::<_, Error>(Json(api::json::patch(
patch_id.into(),
patch,
&repo,
tracking_store,
)))
} }
#[cfg(test)] #[cfg(test)]

View File

@ -16,6 +16,7 @@ use radicle::crypto::ssh::keystore::MemorySigner;
use radicle::crypto::ssh::Keystore; use radicle::crypto::ssh::Keystore;
use radicle::crypto::{KeyPair, Seed, Signer}; use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git::{raw as git2, RefString}; use radicle::git::{raw as git2, RefString};
use radicle::node::tracking::store as TrackingStore;
use radicle::profile::Home; use radicle::profile::Home;
use radicle::storage::ReadStorage; use radicle::storage::ReadStorage;
use radicle::Storage; use radicle::Storage;
@ -83,6 +84,9 @@ pub fn contributor(dir: &Path) -> Context {
} }
fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G) -> Context { fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G) -> Context {
let tracking_db = dir.join("radicle").join("node").join("tracking.db");
TrackingStore::Config::open(tracking_db).unwrap();
let workdir = dir.join("hello-world"); let workdir = dir.join("hello-world");
env::set_var("RAD_COMMIT_TIME", TIMESTAMP.to_string()); env::set_var("RAD_COMMIT_TIME", TIMESTAMP.to_string());

View File

@ -254,6 +254,14 @@ impl Config {
} }
Ok(Box::new(entries.into_iter())) Ok(Box::new(entries.into_iter()))
} }
/// Retrieve `alias` of given node.
/// Calls `Self::node_policy` under the hood.
pub fn alias(&self, nid: &NodeId) -> Option<String> {
self.node_policy(nid)
.map(|node| node.and_then(|n| n.alias))
.unwrap_or(None)
}
} }
#[cfg(test)] #[cfg(test)]