use super::external::{ExternalGitHubService as GitHubService, ExternalService};
use crate::protocol::{ServiceRequest, ServiceResponse};
use crate::services::git::{self, parse_github_url};
use anyhow::{Context, Result};
use duct::cmd;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};
use super::zellij_events;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct FilePRInput {
pub title: String,
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FilePROutput {
pub pr_url: String,
pub pr_number: u64,
pub head_branch: String,
pub base_branch: String,
pub created: bool, }
fn get_remote_url() -> Result<String> {
let output = cmd!("git", "remote", "get-url", "origin")
.read()
.context("Failed to get git remote URL")?;
Ok(output.trim().to_string())
}
fn get_owner_repo() -> Result<(String, String)> {
let remote_url = get_remote_url()?;
parse_github_url(&remote_url)
.ok_or_else(|| anyhow::anyhow!("Failed to parse GitHub URL from remote: {}", remote_url))
}
async fn check_existing_pr_async(
github: &GitHubService,
owner: &str,
repo: &str,
head_branch: &str,
) -> Result<Option<(String, u64, String, String)>> {
info!(
"[FilePR] Checking for existing PR for branch: {}",
head_branch
);
let head_filter = format!("{}:{}", owner, head_branch);
let req = ServiceRequest::GitHubListPullRequests {
owner: owner.into(),
repo: repo.into(),
state: Some("open".to_string()),
limit: Some(10),
head: Some(head_filter),
};
let response = github
.call(req)
.await
.map_err(|e| anyhow::anyhow!("GitHub API error checking for existing PR: {}", e))?;
match response {
ServiceResponse::GitHubPullRequests { pull_requests } => {
if let Some(pr) = pull_requests.first() {
debug!("[FilePR] Found existing PR: {} (#{}) ", pr.url, pr.number);
Ok(Some((
pr.url.clone(),
pr.number as u64,
pr.head_ref_name.clone(),
pr.base_ref_name.clone(),
)))
} else {
debug!("[FilePR] No existing PR found");
Ok(None)
}
}
_ => Err(anyhow::anyhow!("Unexpected response type from GitHub API")),
}
}
async fn create_pr_async(
github: &GitHubService,
owner: &str,
repo: &str,
head_branch: &str,
input: &FilePRInput,
) -> Result<FilePROutput> {
info!("[FilePR] Creating new PR: {}", input.title);
info!("[FilePR] Head branch: {}", head_branch);
let base_branch = "main".to_string();
let req = ServiceRequest::GitHubCreatePR {
owner: owner.into(),
repo: repo.into(),
title: input.title.clone(),
body: input.body.clone(),
head: head_branch.to_string(),
base: base_branch.clone(),
};
let response = github
.call(req)
.await
.map_err(|e| anyhow::anyhow!("GitHub API error creating PR: {}", e))?;
match response {
ServiceResponse::GitHubPR {
number,
url,
head_ref_name,
base_ref_name,
..
} => {
let pr_number = number as u64;
info!("[FilePR] Created PR: {} (#{}) ", url, pr_number);
if let Ok(session) = std::env::var("ZELLIJ_SESSION_NAME") {
if let Some(agent_id_str) = git::extract_agent_id(head_branch) {
match crate::ui_protocol::AgentId::try_from(agent_id_str) {
Ok(agent_id) => {
let event = crate::ui_protocol::AgentEvent::PrFiled {
agent_id,
pr_number,
timestamp: zellij_events::now_iso8601(),
};
if let Err(e) = zellij_events::emit_event(&session, &event) {
warn!("Failed to emit pr:filed event: {}", e);
}
}
Err(e) => {
warn!(
"Invalid agent_id in branch '{}', skipping event: {}",
head_branch, e
);
}
}
}
}
Ok(FilePROutput {
pr_url: url,
pr_number,
head_branch: head_ref_name,
base_branch: base_ref_name,
created: true,
})
}
_ => Err(anyhow::anyhow!("Unexpected response type from GitHub API")),
}
}
async fn file_pr_async(input: &FilePRInput) -> Result<FilePROutput> {
info!("[FilePR] Starting file_pr operation (octocrab)");
let (owner, repo) = get_owner_repo()?;
info!("[FilePR] Repository: {}/{}", owner, repo);
let head_branch = git::get_current_branch()?;
info!("[FilePR] Current branch: {}", head_branch);
let github = GitHubService::from_env().context("GITHUB_TOKEN not set or invalid")?;
if let Some((url, number, head, base)) =
check_existing_pr_async(&github, &owner, &repo, &head_branch).await?
{
info!("[FilePR] PR already exists: {} (#{}) ", url, number);
return Ok(FilePROutput {
pr_url: url,
pr_number: number,
head_branch: head,
base_branch: base,
created: false,
});
}
create_pr_async(&github, &owner, &repo, &head_branch, input).await
}
pub fn file_pr(input: &FilePRInput) -> Result<FilePROutput> {
match tokio::runtime::Handle::try_current() {
Ok(handle) => handle.block_on(file_pr_async(input)),
Err(_) => {
let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
rt.block_on(file_pr_async(input))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_github_url() {
let result = parse_github_url("git@github.com:owner/repo.git");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
let result = parse_github_url("https://github.com/owner/repo.git");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
let result = parse_github_url("https://github.com/owner/repo");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
}