From 2157a294a4df86eb600aa433b2b1ab782c14cbb8 Mon Sep 17 00:00:00 2001 From: Ackerley Tng Date: Wed, 25 Mar 2026 22:14:44 -0700 Subject: [PATCH 1/4] cli: validate that --repo points to a valid git remote Added validation for the --repo argument using git ls-remote to ensure the repository is reachable and valid before proceeding with the submission. Signed-off-by: Ackerley Tng --- src/bin/sashiko-cli.rs | 68 ++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/src/bin/sashiko-cli.rs b/src/bin/sashiko-cli.rs index a95fba8f..e909b36d 100644 --- a/src/bin/sashiko-cli.rs +++ b/src/bin/sashiko-cli.rs @@ -20,8 +20,9 @@ use sashiko::api::{PatchsetsResponse, SubmitRequest, SubmitResponse}; use sashiko::settings::Settings; use serde_json::Value; use std::io::{IsTerminal, Read, Write}; -use std::path::PathBuf; +use std::path::Path; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; +use tokio::process::Command; #[derive(Parser)] #[command(name = "sashiko-cli")] @@ -59,9 +60,9 @@ enum Commands { #[arg(long, value_enum)] r#type: Option, - /// Override repository path (defaults to settings) + /// Override repository path (defaults to settings, can be a local path, URL, or remote name) #[arg(long, short = 'r')] - repo: Option, + repo: Option, /// Baseline commit (for mbox injection only) #[arg(long)] @@ -190,12 +191,16 @@ async fn handle_submit( base_url: &str, input: Option, explicit_type: Option, - repo: Option, + repo: Option, baseline: Option, skip_subjects: Option>, only_subjects: Option>, format: OutputFormat, ) -> Result<()> { + if let Some(r) = &repo { + validate_remote(r).await?; + } + let url = format!("{}/api/submit", base_url); // DWIM Detection Logic @@ -208,12 +213,12 @@ async fn handle_submit( (SubmitType::Mbox, s) } else if s.contains("..") { (SubmitType::Range, s) - } else if PathBuf::from(&s).exists() { + } else if Path::new(&s).exists() { // If it's a file, assume mbox. If it's a dir, maybe repo? // For safety, if it looks like a commit (hex), prefer Remote unless file exists. // But filenames can look like anything. // Sashiko deals with mbox files primarily. - let p = PathBuf::from(&s); + let p = Path::new(&s); if p.is_file() { (SubmitType::Mbox, s) } else { @@ -255,26 +260,18 @@ async fn handle_submit( only_subjects: only_subjects.clone(), } } - SubmitType::Remote => { - let repo_path = repo.map(|p| p.to_string_lossy().to_string()); - - SubmitRequest::Remote { - sha: target, - repo: repo_path, - skip_subjects: skip_subjects.clone(), - only_subjects: only_subjects.clone(), - } - } - SubmitType::Range => { - let repo_path = repo.map(|p| p.to_string_lossy().to_string()); - - SubmitRequest::RemoteRange { - sha: target, - repo: repo_path, - skip_subjects: skip_subjects.clone(), - only_subjects: only_subjects.clone(), - } - } + SubmitType::Remote => SubmitRequest::Remote { + sha: target, + repo: repo.clone(), + skip_subjects: skip_subjects.clone(), + only_subjects: only_subjects.clone(), + }, + SubmitType::Range => SubmitRequest::RemoteRange { + sha: target, + repo: repo.clone(), + skip_subjects: skip_subjects.clone(), + only_subjects: only_subjects.clone(), + }, }; let resp = client.post(&url).json(&payload).send().await?; @@ -578,3 +575,22 @@ fn format_timestamp(ts: i64) -> String { _ => ts.to_string(), } } + +async fn validate_remote(repo: &str) -> Result<()> { + // Run git ls-remote HEAD to see if it's reachable and a valid repo + let status = Command::new("git") + .args(["ls-remote", repo, "HEAD"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await + .context("Failed to run git command")?; + + if !status.success() { + return Err(anyhow::anyhow!( + "Could not reach or validate git remote: {}", + repo + )); + } + Ok(()) +} From 96e162405783c245072c1e625bb6a59ec2368649 Mon Sep 17 00:00:00 2001 From: Ackerley Tng Date: Wed, 25 Mar 2026 22:14:53 -0700 Subject: [PATCH 2/4] cli: resolve revision ranges to full SHAs when --repo is provided When a repository is specified, the CLI now resolves all revision ranges and commits to their full 40-character SHAs before submission. This ensures the server receives unambiguous references and verifies that the submitted commits exist in the provided repository. Signed-off-by: Ackerley Tng --- src/bin/sashiko-cli.rs | 84 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/bin/sashiko-cli.rs b/src/bin/sashiko-cli.rs index e909b36d..387f7d30 100644 --- a/src/bin/sashiko-cli.rs +++ b/src/bin/sashiko-cli.rs @@ -204,7 +204,7 @@ async fn handle_submit( let url = format!("{}/api/submit", base_url); // DWIM Detection Logic - let (submission_type, target) = if let Some(t) = explicit_type { + let (submission_type, mut target) = if let Some(t) = explicit_type { (t, input.unwrap_or_else(|| "HEAD".to_string())) } else { // Auto-detect based on input @@ -241,6 +241,42 @@ async fn handle_submit( } }; + // Resolve SHAs if --repo is provided + if let Some(r) = &repo + && (submission_type == SubmitType::Remote || submission_type == SubmitType::Range) + { + let context = if Path::new(r).is_dir() { + Some(r.as_str()) + } else { + None + }; + + if target.contains("..") { + let parts: Vec<&str> = target.split("..").collect(); + if parts.len() == 2 { + let start = resolve_ref(context, parts[0]).await?; + let end = resolve_ref(context, parts[1]).await?; + + // Check if SHAs exist in the remote (if remote is not the context) + if context.is_none() || context != Some(r) { + verify_sha_exists_in_remote(r, &start).await?; + verify_sha_exists_in_remote(r, &end).await?; + } + + target = format!("{}..{}", start, end); + } + } else { + let sha = resolve_ref(context, &target).await?; + + // Check if SHA exists in the remote (if remote is not the context) + if context.is_none() || context != Some(r) { + verify_sha_exists_in_remote(r, &sha).await?; + } + + target = sha; + } + } + let payload = match submission_type { SubmitType::Mbox => { let content = if target == "-" { @@ -594,3 +630,49 @@ async fn validate_remote(repo: &str) -> Result<()> { } Ok(()) } + +async fn resolve_ref(context: Option<&str>, reference: &str) -> Result { + let mut cmd = Command::new("git"); + if let Some(c) = context { + cmd.arg("-C").arg(c); + } + let output = cmd + .args([ + "rev-parse", + "--verify", + &format!("{}^{{commit}}", reference), + ]) + .output() + .await + .context("Failed to run git rev-parse")?; + + if !output.status.success() { + return Err(anyhow::anyhow!( + "Failed to resolve '{}' to a commit SHA: {}", + reference, + String::from_utf8_lossy(&output.stderr).trim() + )); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +async fn verify_sha_exists_in_remote(repo: &str, sha: &str) -> Result<()> { + // Try git fetch --dry-run + // This is the most reliable check for a remote SHA existence + let status = Command::new("git") + .args(["fetch", "--dry-run", repo, sha]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await + .context("Failed to run git fetch")?; + + if !status.success() { + return Err(anyhow::anyhow!( + "Commit {} was not found in remote repository {}", + sha, + repo + )); + } + Ok(()) +} From aa53a8894c344f7d24756b3f8009d64774c7d38a Mon Sep 17 00:00:00 2001 From: Ackerley Tng Date: Wed, 25 Mar 2026 22:23:41 -0700 Subject: [PATCH 3/4] docs: document sashiko-cli --repo flag and behavior Document that --repo must be a valid git remote (URL or local path acting as a remote). Clarify that if --repo is provided, references are resolved to full 40-character SHAs locally and verified at the remote. If omitted, resolution happens on the server side using its local repository. Added examples for both cases. Signed-off-by: Ackerley Tng --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 428ae97c..cf5c0976 100644 --- a/README.md +++ b/README.md @@ -163,10 +163,26 @@ cargo run --bin sashiko-cli -- [COMMAND] **Commands:** -- **`submit [INPUT]`**: Submit a patch or range for review. +- **`submit [INPUT] [--repo REPO]`**: Submit a patch or range for review. - `INPUT` can be a file path (mbox), a commit SHA, or a range (e.g., `HEAD~3..HEAD`). - If `INPUT` is omitted and stdin is piped, it reads an mbox from stdin. - If `INPUT` is omitted and stdin is a terminal, it defaults to the current `HEAD`. + - `--repo` (or `-r`) specifies an external git repository (URL or local path acting as a git remote). + - If `--repo` is provided, the CLI resolves references to full 40-character SHAs locally and verifies their existence in the remote before submission. + - If `--repo` is omitted, the Sashiko server's local repository is used, and resolution occurs on the server side. + +**Examples:** + +```bash +# Submit a range of commits (resolved on the server side) +sashiko-cli submit HEAD~3..HEAD + +# Submit a range from a remote URL (resolved on the client side) +sashiko-cli submit v6.1..v6.2 --repo https://github.com/torvalds/linux.git + +# Submit using a local path acting as a git remote (e.g. bare repo) +sashiko-cli submit main --repo /path/to/linux.git +``` - **`status`**: Show the current server status and queue statistics. - **`list [FILTER]`**: List recent patchsets. - `FILTER` can be a status (e.g., `pending`, `failed`, `reviewed`) or a search term. From 1d72fb1370f4b774a68345b333c1e78c37eeac20 Mon Sep 17 00:00:00 2001 From: Ackerley Tng Date: Wed, 25 Mar 2026 22:28:07 -0700 Subject: [PATCH 4/4] docs: clarify server-side reference resolution in README Explicitly state that omitting --repo causes references like HEAD to be resolved on the server side using the repository configured in Settings.toml. Signed-off-by: Ackerley Tng --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf5c0976..6b2b40fb 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ cargo run --bin sashiko-cli -- [COMMAND] - If `INPUT` is omitted and stdin is a terminal, it defaults to the current `HEAD`. - `--repo` (or `-r`) specifies an external git repository (URL or local path acting as a git remote). - If `--repo` is provided, the CLI resolves references to full 40-character SHAs locally and verifies their existence in the remote before submission. - - If `--repo` is omitted, the Sashiko server's local repository is used, and resolution occurs on the server side. + - If `--repo` is omitted, references (such as `HEAD`) are resolved on the server side using the local repository configured in the server's `Settings.toml` (`git.repository_path`). **Examples:**