Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Plumb annotated-tag selection through the backend
Daniel Norman committed 7 days ago
commit f8346efff9d15d9ebc66153af8798e59a39bb451
parent de62ac0beb4c0f3b8f7968ec557cde5c97b7605b
10 files changed +129 -4
modified crates/radicle-tauri/src/commands/cob/release.rs
@@ -64,9 +64,10 @@ pub async fn create_or_open_release(
    ctx: tauri::State<'_, AppState>,
    rid: RepoId,
    oid: git::Oid,
+
    tag: Option<git::Oid>,
) -> Result<String, Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.create_or_open_release(rid, oid))
+
    tauri::async_runtime::spawn_blocking(move || ctx.create_or_open_release(rid, oid, tag))
        .await
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
}
modified crates/radicle-tauri/src/commands/repo.rs
@@ -122,6 +122,17 @@ pub fn repo_commit(
}

#[tauri::command]
+
pub async fn list_tags(
+
    ctx: tauri::State<'_, AppState>,
+
    rid: RepoId,
+
) -> Result<Vec<types::repo::Tag>, Error> {
+
    let ctx = ctx.inner().clone();
+
    tauri::async_runtime::spawn_blocking(move || ctx.list_tags(rid))
+
        .await
+
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
}
+

+
#[tauri::command]
pub fn seed(ctx: tauri::State<AppState>, rid: RepoId) -> Result<(), Error> {
    ctx.seed(rid)
}
modified crates/radicle-tauri/src/lib.rs
@@ -76,6 +76,7 @@ pub fn run() {
            repo::diff_stats,
            repo::list_commits,
            repo::list_repo_commits,
+
            repo::list_tags,
            repo::list_repos,
            repo::list_repos_summary,
            repo::repo_by_id,
modified crates/radicle-types/bindings/cob/release/Release.ts
@@ -4,6 +4,11 @@ import type { Artifact } from "./Artifact";
export type Release = {
  id: string;
  oid: string;
+
  /**
+
   * OID of the annotated tag that was recorded alongside the commit,
+
   * if any. `None` means the release was created from a bare commit.
+
   */
+
  tag?: string;
  timestamp: number;
  artifacts: Array<Artifact>;
};
added crates/radicle-types/bindings/repo/Tag.ts
@@ -0,0 +1,22 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type Tag = {
+
  /**
+
   * Short tag refname, e.g. `v1.0.0`.
+
   */
+
  name: string;
+
  /**
+
   * Tag OID for annotated tags, commit OID for lightweight tags.
+
   * This is the OID stored on the artifact COB's `tag` field for
+
   * annotated tags.
+
   */
+
  oid: string;
+
  /**
+
   * The commit this tag points at. For lightweight tags this equals
+
   * `oid`; for annotated tags it's the commit reachable via the tag
+
   * object's target.
+
   */
+
  commit: string;
+
  annotated: boolean;
+
  message?: string;
+
};
modified crates/radicle-types/src/cobs/release.rs
@@ -17,6 +17,11 @@ pub struct Release {
    pub id: radicle_artifact::ReleaseId,
    #[ts(as = "String")]
    pub oid: radicle::git::Oid,
+
    /// OID of the annotated tag that was recorded alongside the commit,
+
    /// if any. `None` means the release was created from a bare commit.
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    #[ts(as = "Option<String>", optional)]
+
    pub tag: Option<radicle::git::Oid>,
    #[ts(type = "number")]
    pub timestamp: u64,
    pub artifacts: Vec<Artifact>,
@@ -84,6 +89,7 @@ impl Release {
        Self {
            id,
            oid: *release.oid(),
+
            tag: release.tag().copied(),
            timestamp: release.timestamp(),
            artifacts,
        }
modified crates/radicle-types/src/repo.rs
@@ -51,6 +51,27 @@ pub struct RepoInfo {
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "repo/")]
+
pub struct Tag {
+
    /// Short tag refname, e.g. `v1.0.0`.
+
    pub name: String,
+
    /// Tag OID for annotated tags, commit OID for lightweight tags.
+
    /// This is the OID stored on the artifact COB's `tag` field for
+
    /// annotated tags.
+
    pub oid: String,
+
    /// The commit this tag points at. For lightweight tags this equals
+
    /// `oid`; for annotated tags it's the commit reachable via the tag
+
    /// object's target.
+
    pub commit: String,
+
    pub annotated: bool,
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    #[ts(optional)]
+
    pub message: Option<String>,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
pub struct Readme {
    #[ts(as = "String")]
    pub id: surf::Oid,
modified crates/radicle-types/src/traits/release_mut.rs
@@ -12,12 +12,18 @@ pub trait ReleasesMut: Releases {
    /// Find a release for the commit OID, or create it. Returns the
    /// release id as a string for the frontend. Idempotent.
    ///
+
    /// `tag` is recorded on the release COB when the user selects an
+
    /// annotated tag for the release; for lightweight tags or bare
+
    /// commit OIDs pass `None`. Only honoured when creating a release —
+
    /// when an existing one is reused, its tag stays as-is.
+
    ///
    /// radicle-artifact 0.12 dropped its built-in `find_or_create_by_oid`
    /// in favour of an explicit two-step pattern; we recreate that here.
    fn create_or_open_release(
        &self,
        rid: identity::RepoId,
        oid: radicle::git::Oid,
+
        tag: Option<radicle::git::Oid>,
    ) -> Result<String, Error> {
        let profile = self.profile();
        let signer = profile.signer()?;
@@ -38,7 +44,7 @@ pub trait ReleasesMut: Releases {
            Some(id) => id,
            None => {
                let release = releases
-
                    .create(oid, None, &signer)
+
                    .create(oid, tag, &signer)
                    .map_err(|e| Error::Iroh(e.to_string()))?;
                *release.id()
            }
modified crates/radicle-types/src/traits/repo.rs
@@ -480,6 +480,48 @@ pub trait Repo: Profile {
        Ok(commit.into())
    }

+
    /// List the repo's tags. Used by the New Release form so users can
+
    /// pick an annotated tag (which records its OID alongside the commit
+
    /// on the artifact COB) instead of typing a raw commit OID.
+
    fn list_tags(&self, rid: identity::RepoId) -> Result<Vec<repo::Tag>, Error> {
+
        use radicle_surf::{Glob, Tag};
+

+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let surf_repo = surf::Repository::open(repo.path())?;
+

+
        let mut tags = Vec::new();
+
        for tag in surf_repo.tags(&Glob::all_tags())? {
+
            let Ok(tag) = tag else { continue };
+
            let entry = match tag {
+
                Tag::Light { id, name } => repo::Tag {
+
                    name: name.to_string(),
+
                    oid: id.to_string(),
+
                    commit: id.to_string(),
+
                    annotated: false,
+
                    message: None,
+
                },
+
                Tag::Annotated {
+
                    id,
+
                    target,
+
                    name,
+
                    message,
+
                    ..
+
                } => repo::Tag {
+
                    name: name.to_string(),
+
                    oid: id.to_string(),
+
                    commit: target.to_string(),
+
                    annotated: true,
+
                    message,
+
                },
+
            };
+
            tags.push(entry);
+
        }
+
        // Newest tag first by name (semver-ish sort) — surf already returns
+
        // them sorted; keep the natural order as-is.
+
        Ok(tags)
+
    }
+

    fn unseed(&self, rid: identity::RepoId) -> Result<(), Error> {
        let profile = self.profile();
        let mut node = radicle::Node::new(profile.home().socket_from_env());
modified crates/test-http-api/src/api.rs
@@ -106,6 +106,7 @@ pub fn router(ctx: Context) -> Router {
        .route("/save_embed_by_bytes", post(save_embed_handler))
        .route("/save_embed_to_disk", post(save_embed_handler))
        .route("/list_jobs", post(jobs_handler))
+
        .route("/list_tags", post(list_tags_handler))
        .route("/list_releases", post(list_releases_handler))
        .route("/release_by_id", post(release_by_id_handler))
        .route("/releases_by_commit", post(releases_by_commit_handler))
@@ -624,6 +625,13 @@ async fn jobs_handler(
    Ok::<_, Error>(Json(jobs))
}

+
async fn list_tags_handler(
+
    State(ctx): State<Context>,
+
    Json(RepoBody { rid }): Json<RepoBody>,
+
) -> impl IntoResponse {
+
    Ok::<_, Error>(Json(ctx.list_tags(rid)?))
+
}
+

#[derive(Serialize, Deserialize)]
struct RidBody {
    pub rid: identity::RepoId,
@@ -650,6 +658,8 @@ struct ComputeCidBody {
struct CreateOrOpenReleaseBody {
    pub rid: identity::RepoId,
    pub oid: git::Oid,
+
    #[serde(default)]
+
    pub tag: Option<git::Oid>,
}

#[derive(Serialize, Deserialize)]
@@ -718,9 +728,9 @@ async fn compute_artifact_cid_handler(

async fn create_or_open_release_handler(
    State(ctx): State<Context>,
-
    Json(CreateOrOpenReleaseBody { rid, oid }): Json<CreateOrOpenReleaseBody>,
+
    Json(CreateOrOpenReleaseBody { rid, oid, tag }): Json<CreateOrOpenReleaseBody>,
) -> impl IntoResponse {
-
    Ok::<_, Error>(Json(ctx.create_or_open_release(rid, oid)?))
+
    Ok::<_, Error>(Json(ctx.create_or_open_release(rid, oid, tag)?))
}

async fn add_artifact_handler(