From: Arnaldo Carvalho de Melo <acme@kernel.org>
To: Roman Gushchin <roman.gushchin@linux.dev>
Cc: sashiko@lists.linux.dev, Derek Barbosa <debarbos@redhat.com>,
Clark Williams <williams@redhat.com>
Subject: [PATCH 1/1] cli: Add cancel command to abort pending reviews
Date: Sun, 19 Apr 2026 11:39:28 -0300 [thread overview]
Message-ID: <aeTpIFdgGhuyYgcZ@x1> (raw)
Hi,
I still need to setup a github repo, see if this is acceptable,
first patch, ran clippy, rust-fmt, tests passed, works for me :-)
- Arnaldo
---
Add `sashiko-cli cancel <id>` to cancel patchsets in Pending or
Incomplete state. With `--force`, also cancels patchsets already
In Review. The reviewer now checks for Cancelled status before
writing its final result, preventing a force-cancelled review
from being overwritten.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
src/api.rs | 46 ++++++++++++++++++++++++++++++++++++++
src/bin/sashiko-cli.rs | 50 ++++++++++++++++++++++++++++++++++++++++++
src/db.rs | 25 +++++++++++++++++++++
src/reviewer.rs | 24 +++++++++++++-------
4 files changed, 137 insertions(+), 8 deletions(-)
diff --git a/src/api.rs b/src/api.rs
index 2c132b7cd0e6a469..d43f225c37fe1e6e 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -186,6 +186,13 @@ pub struct SubsystemQuery {
pub subsystem_id: Option<i64>,
}
+#[derive(Deserialize)]
+pub struct CancelQuery {
+ pub id: i64,
+ #[serde(default)]
+ pub force: bool,
+}
+
#[derive(Deserialize)]
pub struct InjectRequest {
pub raw: String,
@@ -267,6 +274,7 @@ pub async fn run_server(
.route("/api/stats/tools", get(stats_tools))
.route("/api/submit", post(submit_patch))
.route("/api/patchset/rerun", post(rerun_patchset))
+ .route("/api/patchset/cancel", post(cancel_patchset))
.route("/api/patch/rerun", post(rerun_patch))
.route("/", get_service(ServeFile::new("static/index.html")))
.nest_service("/static", ServeDir::new("static"))
@@ -931,6 +939,44 @@ async fn rerun_patchset(
Ok(Json(serde_json::json!({ "status": "accepted" })))
}
+async fn cancel_patchset(
+ ConnectInfo(addr): ConnectInfo<SocketAddr>,
+ State(state): State<Arc<AppState>>,
+ Query(query): Query<CancelQuery>,
+) -> Result<Json<serde_json::Value>, StatusCode> {
+ if state.read_only {
+ return Err(StatusCode::FORBIDDEN);
+ }
+
+ if !state.allow_all_submit && !addr.ip().is_loopback() {
+ return Err(StatusCode::FORBIDDEN);
+ }
+
+ let cancelled = state
+ .db
+ .cancel_patchset(query.id, query.force)
+ .await
+ .map_err(|e| {
+ error!("Failed to cancel patchset {}: {}", query.id, e);
+ StatusCode::INTERNAL_SERVER_ERROR
+ })?;
+
+ if cancelled {
+ info!("Patchset {} cancelled (force={})", query.id, query.force);
+ Ok(Json(serde_json::json!({ "status": "cancelled" })))
+ } else {
+ let reason = if query.force {
+ "Patchset is not in a cancellable state (must be Pending, Incomplete, or In Review)"
+ } else {
+ "Patchset is not in a cancellable state (must be Pending or Incomplete; use force=true for In Review)"
+ };
+ Ok(Json(serde_json::json!({
+ "status": "not_modified",
+ "reason": reason
+ })))
+ }
+}
+
async fn rerun_patch(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(state): State<Arc<AppState>>,
diff --git a/src/bin/sashiko-cli.rs b/src/bin/sashiko-cli.rs
index 09a2ce63fad44376..4a60dc51df0f8567 100644
--- a/src/bin/sashiko-cli.rs
+++ b/src/bin/sashiko-cli.rs
@@ -97,6 +97,15 @@ enum Commands {
#[arg(default_value = "latest")]
id: String,
},
+ /// Cancel a pending review
+ Cancel {
+ /// ID of the patchset to cancel
+ id: i64,
+
+ /// Force cancel even if the review is already in progress
+ #[arg(long, short)]
+ force: bool,
+ },
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
@@ -182,6 +191,7 @@ async fn run_command(
per_page,
} => handle_list(client, base_url, page, per_page, filter, format).await,
Commands::Show { id } => handle_show(client, base_url, id, format).await,
+ Commands::Cancel { id, force } => handle_cancel(client, base_url, id, force, format).await,
}
}
@@ -389,6 +399,7 @@ async fn handle_list(
"Embargoed" => Color::Magenta,
"Failed" | "Error" | "Failed To Apply" => Color::Red,
"Pending" | "In Review" => Color::Yellow,
+ "Cancelled" => Color::Red,
_ => Color::White,
};
@@ -672,6 +683,45 @@ async fn handle_show(
Ok(())
}
+async fn handle_cancel(
+ client: &Client,
+ base_url: &str,
+ id: i64,
+ force: bool,
+ format: OutputFormat,
+) -> Result<()> {
+ let url = format!("{}/api/patchset/cancel?id={}&force={}", base_url, id, force);
+ let resp = client.post(&url).send().await?;
+
+ if resp.status().is_success() {
+ let result: serde_json::Value = resp.json().await?;
+ match format {
+ OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&result)?),
+ OutputFormat::Text => {
+ let status = result["status"].as_str().unwrap_or("");
+ if status == "cancelled" {
+ print_colored(Color::Green, "Cancelled: ");
+ println!("Patchset {} has been cancelled.", id);
+ } else {
+ print_colored(Color::Yellow, "Not modified: ");
+ println!(
+ "{}",
+ result["reason"]
+ .as_str()
+ .unwrap_or("Patchset could not be cancelled.")
+ );
+ }
+ }
+ }
+ } else {
+ let status = resp.status();
+ let text = resp.text().await.unwrap_or_default();
+ return Err(anyhow::anyhow!("Cancel failed ({}): {}", status, text));
+ }
+
+ Ok(())
+}
+
fn print_colored(color: Color, text: &str) {
let mut stdout = StandardStream::stdout(ColorChoice::Auto);
stdout
diff --git a/src/db.rs b/src/db.rs
index 60af07cbbb9c5278..178d6c154e4e77d9 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -3205,6 +3205,31 @@ impl Database {
Ok(())
}
+ pub async fn get_patchset_status(&self, id: i64) -> Result<Option<String>> {
+ let mut rows = self
+ .conn
+ .query(
+ "SELECT status FROM patchsets WHERE id = ?",
+ libsql::params![id],
+ )
+ .await?;
+ if let Some(row) = rows.next().await? {
+ Ok(Some(row.get(0)?))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub async fn cancel_patchset(&self, id: i64, force: bool) -> Result<bool> {
+ let query = if force {
+ "UPDATE patchsets SET status = 'Cancelled' WHERE id = ? AND status IN ('Pending', 'Incomplete', 'In Review')"
+ } else {
+ "UPDATE patchsets SET status = 'Cancelled' WHERE id = ? AND status IN ('Pending', 'Incomplete')"
+ };
+ let count = self.conn.execute(query, libsql::params![id]).await?;
+ Ok(count > 0)
+ }
+
pub async fn restart_failed_reviews(&self) -> Result<u64> {
let count = self.conn.execute(
"UPDATE patchsets SET status = 'Pending', failed_reason = NULL WHERE status = 'Failed'",
diff --git a/src/reviewer.rs b/src/reviewer.rs
index d99f23fe93a35731..9007dc289bc86925 100644
--- a/src/reviewer.rs
+++ b/src/reviewer.rs
@@ -644,16 +644,24 @@ impl Reviewer {
// Cleanup worktree here since we kept it alive for reuse
let _ = worktree.remove().await;
- let final_status = if review_success {
- ReviewStatus::Reviewed.as_str().to_string()
+ let current_status = ctx.db.get_patchset_status(patchset_id).await.ok().flatten();
+ if current_status.as_deref() == Some(ReviewStatus::Cancelled.as_str()) {
+ info!(
+ "Patchset {} was cancelled during review, preserving status",
+ patchset_id
+ );
} else {
- ReviewStatus::Failed.as_str().to_string()
- };
+ let final_status = if review_success {
+ ReviewStatus::Reviewed.as_str().to_string()
+ } else {
+ ReviewStatus::Failed.as_str().to_string()
+ };
- let _ = ctx
- .db
- .update_patchset_status(patchset_id, &final_status)
- .await;
+ let _ = ctx
+ .db
+ .update_patchset_status(patchset_id, &final_status)
+ .await;
+ }
} else {
// No baseline found
warn!("No working baseline found for patchset {}", patchset_id);
--
2.53.0
reply other threads:[~2026-04-19 14:39 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=aeTpIFdgGhuyYgcZ@x1 \
--to=acme@kernel.org \
--cc=debarbos@redhat.com \
--cc=roman.gushchin@linux.dev \
--cc=sashiko@lists.linux.dev \
--cc=williams@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox