Resolve the patch target branch in the API

Serve `targetBranch` so the UI can tell where a patch is headed. Editing
a title no longer sends its own target, which reset branch-targeted
patches to the default branch.
This commit is contained in:
Rūdolfs Ošiņš
2026-09-03 16:46:29 +02:00
parent 3ac4249651
commit 3688218060
7 changed files with 28 additions and 16 deletions
@@ -30,9 +30,9 @@ pub async fn list_patches(
) -> Result<types::cobs::PaginatedQuery<Vec<models::patch::Patch>>, Error> {
let profile = ctx.profile();
let aliases = profile.aliases();
let delegates = Vec::from(profile.storage.repository(rid)?.delegates()?);
let doc = profile.storage.repository(rid)?.identity_doc()?;
Ok(sqlite_service.list_paginated(rid, status, skip, take, &delegates, &aliases)?)
Ok(sqlite_service.list_paginated(rid, status, skip, take, &doc, &aliases)?)
}
#[tauri::command]
@@ -5,7 +5,7 @@ import type { Embed } from "../thread/Embed";
import type { Verdict } from "./Verdict";
export type Action =
| { "type": "edit"; title: string; target: string }
| { "type": "edit"; title: string; target: "delegates" | { branch: string } }
| { "type": "label"; labels: Array<string> }
| { "type": "lifecycle"; state: { status: "draft" | "open" | "archived" } }
| { "type": "assign"; assignees: Array<Author> }
@@ -10,6 +10,7 @@ export type Patch = {
base: string;
head: string;
state: State;
targetBranch?: string;
assignees: Array<Author>;
labels: Array<string>;
timestamp: number;
@@ -54,6 +54,9 @@ pub struct Patch {
#[ts(as = "String")]
head: git::Oid,
state: State,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
target_branch: Option<String>,
assignees: Vec<cobs::Author>,
#[ts(as = "Vec<String>")]
labels: Vec<cob::Label>,
@@ -80,7 +83,7 @@ impl Patch {
pub fn new(
id: patch::PatchId,
patch: &patch::Patch,
delegates: &[radicle::identity::Did],
doc: &radicle::identity::Doc,
aliases: &impl AliasStore,
) -> Self {
// Every review on every revision, so the patch list can render the same
@@ -88,6 +91,7 @@ impl Patch {
// tell a review of the current head from one left behind by a later
// push — see the outdated handling in `reviewSummary`. It follows
// `patch.revisions()`, the timeline order `revision_ids` also uses.
let delegates = doc.delegates();
let reviewers = patch
.revisions()
.enumerate()
@@ -110,6 +114,10 @@ impl Patch {
author: cobs::Author::new(patch.author().id(), aliases),
title: patch.title().to_string(),
state: patch.state().clone().into(),
target_branch: patch
.merge_target_branch(doc)
.map(|branch| branch.to_string())
.ok(),
base: *patch.base(),
head: *patch.head(),
assignees: patch
@@ -373,7 +381,7 @@ pub enum Action {
#[serde(rename = "edit")]
Edit {
title: String,
#[ts(as = "String")]
#[ts(type = "\"delegates\" | { branch: string }")]
target: patch::MergeTarget,
},
#[serde(rename = "label")]
@@ -52,7 +52,7 @@ pub trait PatchService {
status: Option<cobs::query::PatchStatus>,
skip: Option<usize>,
take: Option<usize>,
delegates: &[identity::Did],
doc: &identity::Doc,
aliases: &impl AliasStore,
) -> Result<cobs::PaginatedQuery<Vec<models::patch::Patch>>, models::patch::ListPatchesError>
{
@@ -60,9 +60,8 @@ pub trait PatchService {
None => Box::new(self.list(rid)?),
Some(status) => Box::new(self.list_by_status(rid, status.into())?),
};
let summary = |(id, patch): (PatchId, Patch)| {
models::patch::Patch::new(id, &patch, delegates, aliases)
};
let summary =
|(id, patch): (PatchId, Patch)| models::patch::Patch::new(id, &patch, doc, aliases);
match take {
None => Ok(cobs::PaginatedQuery {
+9 -5
View File
@@ -22,9 +22,9 @@ pub trait Patches: Profile {
let patches = profile.patches(&repo)?;
let patch = patches.get(&id.into())?;
let aliases = &profile.aliases();
let delegates = Vec::from(repo.delegates()?);
let doc = repo.identity_doc()?;
let patches =
patch.map(|patch| models::patch::Patch::new(id.into(), &patch, &delegates, aliases));
patch.map(|patch| models::patch::Patch::new(id.into(), &patch, &doc, aliases));
Ok::<_, Error>(patches)
}
@@ -143,7 +143,11 @@ pub trait PatchesMut: Profile {
models::patch::Action::ReviewCommentUnresolve { review, comment } => {
patch.unresolve_review_comment(review, comment)?;
}
models::patch::Action::Edit { title, target } => {
models::patch::Action::Edit { title, target: _ } => {
// Honouring the client's target would retarget a patch
// opened against a non-default branch; `rad patch edit`
// carries it forward too.
let target = patch.target().clone();
patch.edit(Title::try_from(title)?, target)?;
}
models::patch::Action::ReviewEdit {
@@ -281,12 +285,12 @@ pub trait PatchesMut: Profile {
log::error!("Not able to announce changes: {}", e)
}
let delegates = Vec::from(repo.delegates()?);
let doc = repo.identity_doc()?;
Ok::<_, Error>(models::patch::Patch::new(
*patch.id(),
&patch,
&delegates,
&doc,
&aliases,
))
}
+2 -2
View File
@@ -638,10 +638,10 @@ async fn patches_handler(
}): Json<PatchesBody>,
) -> impl IntoResponse {
let aliases = ctx.profile.aliases();
let delegates = Vec::from(ctx.profile.storage.repository(rid)?.delegates()?);
let doc = ctx.profile.storage.repository(rid)?.identity_doc()?;
let page = ctx
.patches
.list_paginated(rid, status, skip, take, &delegates, &aliases)?;
.list_paginated(rid, status, skip, take, &doc, &aliases)?;
Ok::<_, Error>(Json(page))
}