Files
buzz/desktop/src-tauri/src/commands/task_github.rs
2026-09-09 12:43:31 -04:00

113 lines
3.7 KiB
Rust

use std::process::Stdio;
use tokio::io::AsyncReadExt;
async fn github(args: &[&str]) -> Result<serde_json::Value, String> {
let executable = crate::managed_agents::resolve_command("gh")
.ok_or("Install GitHub CLI and run gh auth login to view GitHub tasks")?;
let mut child = tokio::process::Command::new(executable)
.args(args)
.env("GH_PROMPT_DISABLED", "1")
.env("GH_HOST", "github.com")
.env_remove("BUZZ_PRIVATE_KEY")
.env_remove("BUZZ_AUTH_TAG")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|e| e.to_string())?;
let stdout = child.stdout.take().ok_or("Missing GitHub stdout")?;
let stderr = child.stderr.take().ok_or("Missing GitHub stderr")?;
tokio::time::timeout(std::time::Duration::from_secs(45), async {
let mut output = Vec::new();
let mut error = Vec::new();
let mut stdout = stdout.take(8 * 1024 * 1024);
let mut stderr = stderr.take(64 * 1024);
tokio::try_join!(
stdout.read_to_end(&mut output),
stderr.read_to_end(&mut error),
)
.map_err(|e| e.to_string())?;
if output.len() == 8 * 1024 * 1024 || error.len() == 64 * 1024 {
return Err("GitHub response exceeded the preview limit".into());
}
if !child.wait().await.map_err(|e| e.to_string())?.success() {
return Err(String::from_utf8_lossy(&error).trim().to_string());
}
serde_json::from_slice(&output).map_err(|e| e.to_string())
})
.await
.map_err(|_| "GitHub request timed out".to_string())?
}
/// Read a branch's GitHub changes or pull requests using the local gh login.
#[tauri::command]
pub async fn get_task_github(
repository: String,
branch: String,
view: String,
) -> Result<serde_json::Value, String> {
let parts: Vec<_> = repository.split('/').collect();
if parts.len() != 2
|| parts.iter().any(|p| {
p.is_empty()
|| p.starts_with('.')
|| !p
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
})
{
return Err("Expected GitHub owner/repository".into());
}
if branch.is_empty() || branch.len() > 1024 {
return Err("Expected a branch name".into());
}
let prs = github(&[
"pr",
"list",
"--repo",
&repository,
"--head",
&branch,
"--state",
"all",
"--limit",
"20",
"--json",
"number,title,url,state,isDraft,baseRefName,reviewDecision,statusCheckRollup",
])
.await?;
if view == "review" {
return Ok(prs);
}
if view != "changes" {
return Err("Unknown task view".into());
}
let prs = prs.as_array().ok_or("Invalid GitHub PR response")?;
let pr = prs
.iter()
.find(|pr| pr["state"] == "OPEN")
.or_else(|| prs.first());
let base = match pr.and_then(|pr| pr["baseRefName"].as_str()) {
Some(base) => base.to_string(),
None => github(&["repo", "view", &repository, "--json", "defaultBranchRef"]).await?
["defaultBranchRef"]["name"]
.as_str()
.ok_or("Repository has no default branch")?
.to_string(),
};
let encode =
|value: &str| url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>();
github(&[
"api",
"--hostname",
"github.com",
&format!(
"repos/{repository}/compare/{}...{}",
encode(&base),
encode(&branch)
),
])
.await
}