Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add `issues_by_id`, `patches_by_id`, `revisions_by_patch`, `revisions_by_id`
Draft did:key:z6MkkfM3...sVz5 opened 1 year ago

Also provides all the types related to it and a new npm script that allows generating those types quickly.

12 files changed +410 -5 9334ffb3 037ac391
modified package.json
@@ -12,6 +12,7 @@
    "check-js": "scripts/check-js",
    "check-rs": "scripts/check-rs",
    "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
+
    "generate-types": "cargo test --manifest-path ./src-tauri/Cargo.toml && npx prettier ./src-tauri/bindings --write",
    "tauri": "npx tauri"
  },
  "engines": {
added src-tauri/bindings/Comment.ts
@@ -0,0 +1,14 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "./Author";
+
import type { Edit } from "./Edit";
+
import type { Never } from "./Never";
+

+
export type Comment<T = Never> = {
+
  id: string;
+
  author: Author;
+
  edits: Array<Edit>;
+
  reactions: string;
+
  replyTo?: string;
+
  location?: T;
+
  resolved: boolean;
+
};
added src-tauri/bindings/Edit.ts
@@ -0,0 +1,9 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "./Author";
+

+
export type Edit = {
+
  author: Author;
+
  timestamp: bigint;
+
  body: string;
+
  embeds: { name: string; content: string };
+
};
modified src-tauri/bindings/Issue.ts
@@ -1,5 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "./Author";
+
import type { Comment } from "./Comment";
+
import type { Never } from "./Never";

export type Issue = {
  id: string;
@@ -7,6 +9,7 @@ export type Issue = {
  title: string;
  state: { status: "closed"; reason: "other" | "solved" } | { status: "open" };
  assignees: Array<Author>;
+
  discussion: Array<Comment<Never>>;
  labels: Array<string>;
  timestamp: number;
};
added src-tauri/bindings/Never.ts
@@ -0,0 +1,8 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
/**
+
 * The `Infallible` type does not have a `Serialize`/`Deserialize`
+
 * implementation. The `Never` type imitates `Infallible` and
+
 * provides the derived implementations.
+
 */
+
export type Never = never;
added src-tauri/bindings/Reaction.ts
@@ -0,0 +1,9 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "./Author";
+
import type { CodeLocation } from "./CodeLocation";
+

+
export type Reaction = {
+
  emoji: string;
+
  authors: Array<Author>;
+
  location?: CodeLocation;
+
};
added src-tauri/bindings/Review.ts
@@ -0,0 +1,13 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "./Author";
+
import type { CodeLocation } from "./CodeLocation";
+
import type { Comment } from "./Comment";
+

+
export type Review = {
+
  id: string;
+
  author: Author;
+
  verdict?: "accept" | "reject";
+
  summary: string | null;
+
  comments: Array<Comment<CodeLocation>>;
+
  timestamp: bigint;
+
};
added src-tauri/bindings/Revision.ts
@@ -0,0 +1,18 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "./Author";
+
import type { CodeLocation } from "./CodeLocation";
+
import type { Comment } from "./Comment";
+
import type { Edit } from "./Edit";
+
import type { Reaction } from "./Reaction";
+
import type { Review } from "./Review";
+

+
export type Revision = {
+
  author: Author;
+
  description: Array<Edit>;
+
  base: string;
+
  oid: string;
+
  reviews: Array<Review>;
+
  timestamp: bigint;
+
  discussion: Array<Comment<CodeLocation>>;
+
  reactions: Array<Reaction>;
+
};
modified src-tauri/src/commands/auth.rs
@@ -1,4 +1,5 @@
use anyhow::anyhow;
+

use radicle::crypto::ssh;

use crate::{error::Error, AppState};
modified src-tauri/src/commands/cobs.rs
@@ -1,5 +1,10 @@
+
use std::str::FromStr;
+

+
use radicle::cob::ObjectId;
+
use radicle::git::Oid;
use radicle::identity::RepoId;
use radicle::issue::cache::Issues;
+
use radicle::issue::IssueId;
use radicle::patch::cache::Patches;

use crate::error::Error;
@@ -33,6 +38,22 @@ pub fn list_issues(
}

#[tauri::command]
+
pub fn issues_by_id(
+
    ctx: tauri::State<AppState>,
+
    rid: RepoId,
+
    id: IssueId,
+
) -> Result<Option<cobs::Issue>, Error> {
+
    let (repo, _) = ctx.repo(rid)?;
+
    let issues = ctx.profile.issues(&repo)?;
+
    let issue = issues.get(&id)?;
+

+
    let aliases = &ctx.profile.aliases();
+
    let issue = issue.map(|issue| cobs::Issue::new(id, issue, aliases));
+

+
    Ok::<_, Error>(issue)
+
}
+

+
#[tauri::command]
pub fn list_patches(
    ctx: tauri::State<AppState>,
    rid: RepoId,
@@ -58,6 +79,66 @@ pub fn list_patches(
    Ok::<_, Error>(patches)
}

+
#[tauri::command]
+
pub fn patches_by_id(
+
    ctx: tauri::State<AppState>,
+
    rid: RepoId,
+
    id: String,
+
) -> Result<Option<cobs::Patch>, Error> {
+
    let id = ObjectId::from_str(&id)?;
+
    let (repo, _) = ctx.repo(rid)?;
+
    let patches = ctx.profile.patches(&repo)?;
+
    let patch = patches.get(&id)?;
+

+
    let aliases = &ctx.profile.aliases();
+
    let patches = patch.map(|patch| cobs::Patch::new(id, patch, aliases));
+

+
    Ok::<_, Error>(patches)
+
}
+

+
#[tauri::command]
+
pub fn revisions_by_patch(
+
    ctx: tauri::State<AppState>,
+
    rid: RepoId,
+
    id: String,
+
) -> Result<Option<Vec<cobs::Revision>>, Error> {
+
    let id = ObjectId::from_str(&id)?;
+
    let (repo, _) = ctx.repo(rid)?;
+
    let patches = ctx.profile.patches(&repo)?;
+

+
    let revisions = patches.get(&id)?.map(|patch| {
+
        let aliases = &ctx.profile.aliases();
+

+
        patch
+
            .revisions()
+
            .map(|(_, r)| cobs::Revision::new(r.clone(), aliases))
+
            .collect::<Vec<_>>()
+
    });
+

+
    Ok::<_, Error>(revisions)
+
}
+

+
#[tauri::command]
+
pub fn revisions_by_id(
+
    ctx: tauri::State<AppState>,
+
    rid: RepoId,
+
    id: String,
+
    revision_id: String,
+
) -> Result<Option<cobs::Revision>, Error> {
+
    let id = ObjectId::from_str(&id)?;
+
    let (repo, _) = ctx.repo(rid)?;
+
    let patches = ctx.profile.patches(&repo)?;
+
    let revision = patches.get(&id)?.and_then(|patch| {
+
        let revision_id = Oid::from_str(&revision_id).ok()?;
+
        let aliases = &ctx.profile.aliases();
+

+
        patch
+
            .revision(&revision_id.into())
+
            .map(|r| cobs::Revision::new(r.clone(), aliases))
+
    });
+
    Ok::<_, Error>(revision)
+
}
+

mod query {
    use serde::{Deserialize, Serialize};

modified src-tauri/src/lib.rs
@@ -141,9 +141,13 @@ pub fn run() {
            repos::list_repos,
            repos::repo_by_id,
            cobs::list_issues,
+
            cobs::issues_by_id,
            cobs::list_patches,
            thread::create_issue_comment,
            thread::create_patch_comment,
+
            cobs::patches_by_id,
+
            cobs::revisions_by_patch,
+
            cobs::revisions_by_id,
            profile::config,
        ])
        .run(tauri::generate_context!())
modified src-tauri/src/types/cobs.rs
@@ -1,15 +1,19 @@
+
use std::collections::BTreeMap;
+

+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;
+

use radicle::cob;
-
use radicle::git;
+
use radicle::crypto;
use radicle::identity;
use radicle::issue;
use radicle::node::{Alias, AliasStore};
use radicle::patch;
-
use serde::Deserialize;
-
use serde::Serialize;
-
use ts_rs::TS;
+
use radicle::storage::git;

#[derive(Serialize, TS)]
-
pub(crate) struct Author {
+
#[serde(rename_all = "camelCase")]
+
pub struct Author {
    #[ts(as = "String")]
    did: identity::Did,
    #[serde(skip_serializing_if = "Option::is_none")]
@@ -38,6 +42,7 @@ pub struct Issue {
    #[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
    state: issue::State,
    assignees: Vec<Author>,
+
    discussion: Vec<Comment>,
    #[ts(as = "Vec<String>")]
    labels: Vec<cob::Label>,
    #[ts(type = "number")]
@@ -55,6 +60,10 @@ impl Issue {
                .assignees()
                .map(|did| Author::new(*did, aliases))
                .collect::<Vec<_>>(),
+
            discussion: issue
+
                .comments()
+
                .map(|(id, c)| Comment::<Never>::new(*id, c.clone(), aliases))
+
                .collect::<Vec<_>>(),
            labels: issue.labels().cloned().collect::<Vec<_>>(),
            timestamp: issue.timestamp(),
        }
@@ -106,6 +115,230 @@ impl Patch {
    }
}

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
pub struct Revision {
+
    author: Author,
+
    description: Vec<Edit>,
+
    #[ts(as = "String")]
+
    base: git::Oid,
+
    #[ts(as = "String")]
+
    oid: git::Oid,
+
    reviews: Vec<Review>,
+
    #[ts(as = "i64")]
+
    timestamp: cob::common::Timestamp,
+
    discussion: Vec<Comment<CodeLocation>>,
+
    reactions: Vec<Reaction>,
+
}
+

+
impl Revision {
+
    pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            author: Author::new(*value.author().id(), aliases),
+
            description: value
+
                .edits()
+
                .map(|e| Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            base: *value.base(),
+
            oid: value.head(),
+
            reviews: value
+
                .reviews()
+
                .map(|(id, r)| Review::new(*id, r.clone(), aliases))
+
                .collect::<Vec<_>>(),
+
            timestamp: value.timestamp(),
+
            discussion: value
+
                .discussion()
+
                .comments()
+
                .map(|(id, c)| Comment::<CodeLocation>::new(*id, c.clone(), aliases))
+
                .collect::<Vec<_>>(),
+
            reactions: value
+
                .reactions()
+
                .iter()
+
                .flat_map(|(location, reactions)| {
+
                    let reaction_by_author = reactions.iter().fold(
+
                        BTreeMap::new(),
+
                        |mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| {
+
                            acc.entry(emoji).or_default().push(author);
+
                            acc
+
                        },
+
                    );
+
                    reaction_by_author
+
                        .into_iter()
+
                        .map(|(emoji, authors)| {
+
                            Reaction::new(
+
                                *emoji,
+
                                authors,
+
                                location.as_ref().map(|l| CodeLocation::new(l.clone())),
+
                                aliases,
+
                            )
+
                        })
+
                        .collect::<Vec<_>>()
+
                })
+
                .collect::<Vec<_>>(),
+
        }
+
    }
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
pub struct Reaction {
+
    #[ts(as = "String")]
+
    emoji: cob::Reaction,
+
    authors: Vec<Author>,
+
    #[ts(optional)]
+
    location: Option<CodeLocation>,
+
}
+

+
impl Reaction {
+
    pub fn new(
+
        emoji: cob::Reaction,
+
        authors: Vec<&crypto::PublicKey>,
+
        location: Option<CodeLocation>,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            emoji,
+
            authors: authors
+
                .into_iter()
+
                .map(|a| Author::new(a.into(), aliases))
+
                .collect::<Vec<_>>(),
+
            location,
+
        }
+
    }
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Review {
+
    #[ts(as = "String")]
+
    id: identity::PublicKey,
+
    author: Author,
+
    #[ts(type = "'accept' | 'reject'")]
+
    #[ts(optional)]
+
    verdict: Option<cob::patch::Verdict>,
+
    summary: Option<String>,
+
    comments: Vec<Comment<CodeLocation>>,
+
    #[ts(as = "i64")]
+
    timestamp: cob::common::Timestamp,
+
}
+

+
impl Review {
+
    pub fn new(
+
        id: identity::PublicKey,
+
        review: cob::patch::Review,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            id,
+
            author: Author::new(review.author().id, aliases),
+
            verdict: review.verdict(),
+
            summary: review.summary().map(|s| s.to_string()),
+
            comments: review
+
                .comments()
+
                .map(|(id, c)| Comment::<CodeLocation>::new(*id, c.clone(), aliases))
+
                .collect::<Vec<_>>(),
+
            timestamp: review.timestamp(),
+
        }
+
    }
+
}
+

+
#[derive(TS, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Edit {
+
    pub author: Author,
+
    #[ts(as = "i64")]
+
    pub timestamp: cob::common::Timestamp,
+
    pub body: String,
+
    #[ts(type = "{ name: string, content: string }")]
+
    pub embeds: Vec<cob::change::store::Embed<cob::common::Uri>>,
+
}
+

+
impl Edit {
+
    pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            author: Author::new(edit.author.into(), aliases),
+
            timestamp: edit.timestamp,
+
            body: edit.body.clone(),
+
            embeds: edit.embeds.clone(),
+
        }
+
    }
+
}
+

+
/// The `Infallible` type does not have a `Serialize`/`Deserialize`
+
/// implementation. The `Never` type imitates `Infallible` and
+
/// provides the derived implementations.
+
#[derive(TS, Serialize)]
+
pub enum Never {}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Comment<T = Never> {
+
    #[ts(as = "String")]
+
    id: cob::thread::CommentId,
+
    author: Author,
+
    edits: Vec<Edit>,
+
    #[ts(as = "String")]
+
    reactions: BTreeMap<cob::common::Reaction, Vec<cob::op::ActorId>>,
+
    #[ts(as = "Option<String>")]
+
    #[ts(optional)]
+
    reply_to: Option<cob::thread::CommentId>,
+
    #[ts(optional)]
+
    location: Option<T>,
+
    resolved: bool,
+
}
+

+
impl Comment<CodeLocation> {
+
    pub fn new(
+
        id: cob::thread::CommentId,
+
        comment: cob::thread::Comment<cob::common::CodeLocation>,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            id,
+
            author: Author::new(comment.author().into(), aliases),
+
            edits: comment
+
                .edits()
+
                .map(|e| Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            reactions: comment
+
                .reactions()
+
                .into_iter()
+
                .map(|(r, a)| (*r, a.into_iter().copied().collect::<Vec<_>>()))
+
                .collect::<BTreeMap<cob::common::Reaction, Vec<cob::op::ActorId>>>(),
+
            reply_to: comment.reply_to(),
+
            location: comment.location().map(|l| CodeLocation::new(l.clone())),
+
            resolved: comment.is_resolved(),
+
        }
+
    }
+
}
+

+
impl Comment {
+
    pub fn new(
+
        id: cob::thread::CommentId,
+
        comment: cob::thread::Comment,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            id,
+
            author: Author::new(comment.author().into(), aliases),
+
            edits: comment
+
                .edits()
+
                .map(|e| Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            reactions: comment
+
                .reactions()
+
                .into_iter()
+
                .map(|(r, a)| (*r, a.into_iter().copied().collect::<Vec<_>>()))
+
                .collect::<BTreeMap<cob::common::Reaction, Vec<cob::op::ActorId>>>(),
+
            reply_to: comment.reply_to(),
+
            location: None,
+
            resolved: comment.is_resolved(),
+
        }
+
    }
+
}
+

#[derive(TS, Serialize, Deserialize)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@@ -156,6 +389,17 @@ impl From<cob::CodeLocation> for CodeLocation {
    }
}

+
impl CodeLocation {
+
    pub fn new(location: cob::common::CodeLocation) -> Self {
+
        Self {
+
            commit: location.commit,
+
            path: location.path,
+
            old: location.old.map(|l| l.into()),
+
            new: location.new.map(|l| l.into()),
+
        }
+
    }
+
}
+

impl From<CodeLocation> for cob::CodeLocation {
    fn from(val: CodeLocation) -> Self {
        Self {