use anyhow::Result;
use colored::Colorize;
use std::process::Command;
use crate::cli::RemoteStatusArgs;
use crate::store::{self, Store};
pub fn run(store: &Store, args: &RemoteStatusArgs) -> Result<()> {
let ls_out = Command::new("git")
.args(["ls-remote", "origin", "refs/agentdiff/*"])
.current_dir(&store.repo_root)
.output();
let remote_refs: Vec<(String, String)> = match ls_out {
Ok(out) if out.status.success() => {
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| {
let mut parts = line.splitn(2, '\t');
let sha = parts.next()?.trim().to_string();
let refname = parts.next()?.trim().to_string();
Some((sha, refname))
})
.collect()
}
Ok(_) => {
Vec::new()
}
Err(e) => {
anyhow::bail!("git ls-remote failed: {e}");
}
};
let remote_label = Command::new("git")
.args(["remote", "get-url", "origin"])
.current_dir(&store.repo_root)
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
} else {
None
}
})
.unwrap_or_else(|| "origin".to_string());
println!();
println!(
" {} — {}",
"agentdiff remote-status".cyan().bold(),
remote_label.dimmed()
);
println!();
if remote_refs.is_empty() {
println!(" {} no agentdiff refs found on remote", "--".dimmed());
println!();
println!(
" {}",
"Push local traces with: agentdiff push && git push".dimmed()
);
println!();
return Ok(());
}
let hdr = format!(" {:<45} {:<10} {}", "REF", "TRACES", "LOCAL");
println!("{}", hdr.dimmed());
println!(" {}", "─".repeat(72).dimmed());
for (sha, refname) in &remote_refs {
let short_sha = if sha.len() >= 8 { &sha[..8] } else { sha };
let trace_count = if !args.no_fetch {
fetch_trace_count(&store.repo_root, refname)
} else {
None
};
let count_str = match trace_count {
Some(n) => format!("{n}"),
None => short_sha.to_string(),
};
let local_str = local_ref_status(&store, refname);
println!(
" {:<45} {:<10} {}",
refname.cyan(),
count_str,
local_str.dimmed()
);
}
if let Ok(branch) = store.current_branch() {
let local_path = store.local_traces_path(&branch);
if local_path.exists() {
let local_traces = store.load_local_traces(&branch).unwrap_or_default();
let branch_ref = store::branch_ref_name(&branch);
let on_remote = remote_refs.iter().any(|(_, r)| r == &branch_ref);
if !on_remote && !local_traces.is_empty() {
println!();
println!(
" {} {} local trace(s) for '{}' not yet pushed — run: {}",
"!".yellow(),
local_traces.len(),
branch,
"agentdiff push".cyan()
);
}
}
}
println!();
Ok(())
}
fn fetch_trace_count(repo_root: &std::path::Path, ref_name: &str) -> Option<usize> {
let local_count = count_local_ref(repo_root, ref_name);
if local_count.is_some() {
return local_count;
}
store::fetch_ref_content_via_api(repo_root, ref_name, "traces.jsonl")
.ok()
.flatten()
.map(|content| content.lines().filter(|l| !l.trim().is_empty()).count())
}
fn count_local_ref(repo_root: &std::path::Path, ref_name: &str) -> Option<usize> {
let spec = format!("{ref_name}:traces.jsonl");
let out = Command::new("git")
.args(["show", &spec])
.current_dir(repo_root)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let content = String::from_utf8_lossy(&out.stdout);
Some(content.lines().filter(|l| !l.trim().is_empty()).count())
}
fn local_ref_status(store: &Store, ref_name: &str) -> String {
let local_sha = Command::new("git")
.args(["rev-parse", ref_name])
.current_dir(&store.repo_root)
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
} else {
None
}
});
match local_sha {
Some(_) => "synced".to_string(),
None => {
if ref_name.starts_with("refs/agentdiff/traces/") {
let branch_part = ref_name.trim_start_matches("refs/agentdiff/traces/");
let local_path = store.local_traces_path(branch_part);
if local_path.exists() {
return "local buffer only (run: agentdiff push)".to_string();
}
}
"not fetched locally".to_string()
}
}
}