diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index 1edb48ff..e20c6e84 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -69,6 +69,10 @@ pub enum Error { /// Identity doc error. #[error(transparent)] IdentityDoc(#[from] radicle::identity::doc::DocError), + + /// Tracking store error. + #[error(transparent)] + TrackingStore(#[from] radicle::node::tracking::store::Error), } impl IntoResponse for Error { diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs index 996db6ca..2f20e2e0 100644 --- a/radicle-httpd/src/api/json.rs +++ b/radicle-httpd/src/api/json.rs @@ -12,6 +12,7 @@ use radicle::cob::thread; use radicle::cob::thread::{CommentId, Thread}; use radicle::cob::{ActorId, Author, Reaction, Timestamp}; use radicle::git::RefString; +use radicle::node::tracking::store as TrackingStore; use radicle::storage::{git, refs, ReadRepository}; use radicle_surf::blob::Blob; 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`. -pub(crate) fn issue(id: IssueId, issue: Issue) -> Value { +pub(crate) fn issue(id: IssueId, issue: Issue, aliases: &TrackingStore::Config) -> Value { json!({ "id": id.to_string(), - "author": issue.author(), + "author": author(&issue.author(), aliases.alias(issue.author().id())), "title": issue.title(), "state": issue.state(), "assignees": issue.assigned().collect::>(), "discussion": issue .comments() - .map(|(id, comment)| Comment::new(id, comment, issue.thread())) + .map(|(id, comment)| Comment::new(id, comment, issue.thread(), aliases)) .collect::>(), "tags": issue.tags().collect::>(), }) } /// 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!({ "id": id.to_string(), - "author": patch.author(), + "author": author(patch.author(), aliases.alias(patch.author().id())), "title": patch.title(), "description": patch.description(), "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![]), "merges": rev.merges().collect::>(), "discussions": rev.discussion().comments() - .map(|(id, comment)| Comment::new(id, comment, rev.discussion())) + .map(|(id, comment)| Comment::new(id, comment, rev.discussion(), aliases)) .collect::>(), "timestamp": rev.timestamp(), "reviews": rev.reviews().collect::>(), @@ -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) -> Value { + match alias { + Some(alias) => json!({ + "id": author.id, + "alias": alias, + }), + None => json!(author), + } +} + /// Returns the name part of a path string. fn name_in_path(path: &str) -> &str { match path.rsplit('/').next() { @@ -168,7 +185,7 @@ fn get_refs( #[serde(rename_all = "camelCase")] struct Comment<'a> { id: CommentId, - author: Author, + author: Value, body: &'a str, reactions: Vec<(&'a ActorId, &'a Reaction)>, timestamp: Timestamp, @@ -176,10 +193,16 @@ struct 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 { id: *id, - author: Author::new(comment.author()), + author: author(&comment_author, aliases.alias(comment_author.id())), body: comment.body(), reactions: thread.reactions(id).collect::>(), timestamp: comment.timestamp(), diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 0d45592b..ae1c3dae 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -319,6 +319,7 @@ async fn remotes_handler(State(ctx): State, Path(project): Path) -> let storage = &ctx.profile.storage; let repo = storage.repository(project)?; let delegates = repo.delegates()?; + let tracking_store = &ctx.profile.tracking()?; let remotes = repo .remotes()? .filter_map(|r| r.map(|r| r.1).ok()) @@ -333,11 +334,19 @@ async fn remotes_handler(State(ctx): State, Path(project): Path) -> }) .collect::>(); - json!({ - "id": remote.id, - "heads": refs, - "delegate": delegates.contains(&remote.id.into()), - }) + match tracking_store.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::>(); @@ -440,9 +449,10 @@ async fn issues_handler( .collect::>(); issues.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp())); + let tracking_store = &ctx.profile.tracking()?; let issues = issues .into_iter() - .map(|(id, issue, _)| api::json::issue(id, issue)) + .map(|(id, issue, _)| api::json::issue(id, issue, tracking_store)) .skip(page * per_page) .take(per_page) .collect::>(); @@ -559,8 +569,13 @@ async fn issue_handler( let issue = issue::Issues::open(&repo)? .get(&issue_id.into())? .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)] @@ -722,9 +737,10 @@ async fn patches_handler( }) .collect::>(); patches.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp())); + let tracking_store = &ctx.profile.tracking()?; let patches = patches .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) .take(per_page) .collect::>(); @@ -743,8 +759,14 @@ async fn patch_handler( let patch = patch::Patches::open(&repo)? .get(&patch_id.into())? .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)] diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index 650e8d30..b9643197 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -16,6 +16,7 @@ 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::node::tracking::store as TrackingStore; use radicle::profile::Home; use radicle::storage::ReadStorage; use radicle::Storage; @@ -83,6 +84,9 @@ pub fn contributor(dir: &Path) -> Context { } fn seed_with_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"); env::set_var("RAD_COMMIT_TIME", TIMESTAMP.to_string()); diff --git a/radicle/src/node/tracking/store.rs b/radicle/src/node/tracking/store.rs index d1067d7e..4da5fa49 100644 --- a/radicle/src/node/tracking/store.rs +++ b/radicle/src/node/tracking/store.rs @@ -254,6 +254,14 @@ impl Config { } 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 { + self.node_policy(nid) + .map(|node| node.and_then(|n| n.alias)) + .unwrap_or(None) + } } #[cfg(test)]