use crate::cli::args::CiCommands;
use crate::cli::UI;
use crate::platform;
use anyhow::Result;
use std::path::PathBuf;
pub async fn execute(action: CiCommands, ui: &UI) -> Result<()> {
match action {
CiCommands::Status { branch } => status(branch, ui).await,
CiCommands::Watch { branch, interval } => watch(branch, interval, ui).await,
}
}
async fn status(branch: Option<String>, ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let remote = platform::detect_remote(&path)?;
let token = platform::resolve_token(&remote.host)
.ok_or_else(|| anyhow::anyhow!("Not authenticated. Run: securegit auth login"))?;
let client = platform::create_client(&remote, token);
let repo = crate::ops::open_repo(&path)?;
let (branch_name, commit_sha) = resolve_ref(&repo, branch.as_deref())?;
ui.header("SecureGit CI Status");
ui.blank();
ui.field("Branch", &branch_name);
ui.field("Commit", &commit_sha[..std::cmp::min(7, commit_sha.len())]);
ui.field(
"Provider",
format!("{} ({}/{})", remote.host, remote.owner, remote.repo),
);
ui.blank();
let spinner = ui.spinner("Fetching CI status...");
let status = client.get_check_runs(&commit_sha).await?;
ui.finish_progress(&spinner, "");
if status.check_runs.is_empty() {
ui.info("No CI checks found for this commit");
ui.blank();
return Ok(());
}
ui.section("Checks");
let mut passed = 0u32;
let mut failed = 0u32;
let mut pending = 0u32;
for run in &status.check_runs {
let (icon, conclusion_str) = match run.status.as_str() {
"completed" => match run.conclusion.as_deref() {
Some("success") => {
passed += 1;
(true, "passed")
}
Some("failure") => {
failed += 1;
(false, "failed")
}
Some("cancelled") => {
failed += 1;
(false, "cancelled")
}
Some("skipped") => {
passed += 1;
(true, "skipped")
}
Some(other) => {
pending += 1;
(false, other)
}
None => {
pending += 1;
(false, "unknown")
}
},
"in_progress" | "running" => {
pending += 1;
(false, "running")
}
"queued" | "waiting" => {
pending += 1;
(false, "queued")
}
"success" => {
passed += 1;
(true, "passed")
}
"failed" => {
failed += 1;
(false, "failed")
}
_ => {
pending += 1;
(false, &*run.status)
}
};
let duration = format_duration(run.started_at.as_deref(), run.completed_at.as_deref());
let duration_str = if duration.is_empty() {
String::new()
} else {
format!(" {}", duration)
};
ui.status_item(
icon,
format!("{:<20} {:<12}{}", run.name, conclusion_str, duration_str),
);
}
let total = passed + failed + pending;
let overall = if failed > 0 {
"failing"
} else if pending > 0 {
"pending"
} else {
"passing"
};
ui.blank();
ui.field(
"Overall",
format!("{} ({}/{} complete)", overall, passed + failed, total),
);
ui.blank();
if ui.json {
ui.json_out(&serde_json::json!({
"branch": branch_name,
"commit": commit_sha,
"state": status.state,
"total": total,
"passed": passed,
"failed": failed,
"pending": pending,
"checks": status.check_runs,
}));
}
Ok(())
}
async fn watch(branch: Option<String>, interval: u64, ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let remote = platform::detect_remote(&path)?;
let repo = crate::ops::open_repo(&path)?;
let (branch_name, commit_sha) = resolve_ref(&repo, branch.as_deref())?;
ui.header("SecureGit CI Watch");
ui.blank();
ui.field("Branch", &branch_name);
ui.field("Commit", &commit_sha[..std::cmp::min(7, commit_sha.len())]);
ui.info(format!("Polling every {}s (Ctrl+C to stop)", interval));
ui.blank();
loop {
let token = platform::resolve_token(&remote.host)
.ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
let client = platform::create_client(&remote, token);
let status = client.get_check_runs(&commit_sha).await?;
let term = console::Term::stderr();
let _ = term.clear_last_lines(status.check_runs.len() + 3);
let mut all_complete = true;
for run in &status.check_runs {
let (icon, label) = match run.status.as_str() {
"completed" => match run.conclusion.as_deref() {
Some("success") => (true, "passed"),
Some("failure") => (true, "failed"), _ => (false, "done"),
},
_ => {
all_complete = false;
(false, "running")
}
};
let is_pass = icon && label == "passed";
ui.status_item(is_pass, format!("{:<20} {}", run.name, label));
}
ui.blank();
ui.field("State", &status.state);
if all_complete {
ui.blank();
if status.state == "success" {
ui.success("All checks passed");
} else {
ui.error("Some checks failed");
}
break;
}
tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
}
Ok(())
}
fn resolve_ref(repo: &git2::Repository, branch: Option<&str>) -> Result<(String, String)> {
if let Some(branch_name) = branch {
if let Ok(reference) = repo.find_branch(branch_name, git2::BranchType::Local) {
if let Some(target) = reference.get().target() {
return Ok((branch_name.to_string(), target.to_string()));
}
}
if let Ok(obj) = repo.revparse_single(branch_name) {
return Ok((branch_name.to_string(), obj.id().to_string()));
}
anyhow::bail!("Could not resolve branch: {}", branch_name);
}
let head = repo.head()?;
let branch_name = head.shorthand().unwrap_or("HEAD").to_string();
let sha = head.peel_to_commit()?.id().to_string();
Ok((branch_name, sha))
}
fn format_duration(started: Option<&str>, completed: Option<&str>) -> String {
let start = started.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok());
let end = completed.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok());
match (start, end) {
(Some(s), Some(e)) => {
let dur = e.signed_duration_since(s);
let secs = dur.num_seconds();
if secs < 60 {
format!("{}s", secs)
} else {
format!("{}m {}s", secs / 60, secs % 60)
}
}
(Some(_), None) => "...".to_string(),
_ => String::new(),
}
}