agentctl/debug/sources/
git_context.rs1use super::super::bundle::{normalize_relative_path, write_json_artifact};
2use super::super::schema::BundleArtifact;
3use serde::Serialize;
4use std::path::Path;
5use std::process::Command;
6
7pub const ARTIFACT_ID: &str = "git-context";
8pub const ARTIFACT_RELATIVE_PATH: &str = "artifacts/10-git-context.json";
9
10#[derive(Debug, Serialize)]
11struct GitContextArtifact {
12 source: &'static str,
13 cwd: String,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 repo_root: Option<String>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 branch: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 head: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 status_porcelain_v1: Option<String>,
22 #[serde(default, skip_serializing_if = "Vec::is_empty")]
23 errors: Vec<String>,
24}
25
26pub fn collect(output_dir: &Path) -> BundleArtifact {
27 let normalized_relative_path = normalize_relative_path(ARTIFACT_RELATIVE_PATH);
28 let mut errors = Vec::new();
29
30 let cwd = std::env::current_dir()
31 .map(|path| path.to_string_lossy().to_string())
32 .unwrap_or_else(|error| {
33 let message = format!("failed to resolve current directory: {error}");
34 errors.push(message);
35 ".".to_string()
36 });
37
38 let repo_root = run_git(&["rev-parse", "--show-toplevel"]).ok();
39 if repo_root.is_none() {
40 errors.push("failed to resolve git repository root".to_string());
41 }
42
43 let branch = run_git(&["rev-parse", "--abbrev-ref", "HEAD"]).ok();
44 if branch.is_none() {
45 errors.push("failed to resolve git branch".to_string());
46 }
47
48 let head = run_git(&["rev-parse", "HEAD"]).ok();
49 if head.is_none() {
50 errors.push("failed to resolve git HEAD".to_string());
51 }
52
53 let status_porcelain_v1 = run_git(&["status", "--porcelain=v1", "--branch"]).ok();
54 if status_porcelain_v1.is_none() {
55 errors.push("failed to collect git status".to_string());
56 }
57
58 let payload = GitContextArtifact {
59 source: ARTIFACT_ID,
60 cwd,
61 repo_root,
62 branch,
63 head,
64 status_porcelain_v1,
65 errors: errors.clone(),
66 };
67
68 let mut failures = Vec::new();
69 if let Err(error) = write_json_artifact(output_dir, &normalized_relative_path, &payload) {
70 failures.push(format!("failed to persist git context artifact: {error}"));
71 }
72
73 failures.extend(errors);
74
75 if failures.is_empty() {
76 BundleArtifact::collected(ARTIFACT_ID, normalized_relative_path)
77 } else {
78 BundleArtifact::failed(ARTIFACT_ID, normalized_relative_path, failures.join("; "))
79 }
80}
81
82fn run_git(args: &[&str]) -> Result<String, String> {
83 let output = Command::new("git")
84 .args(args)
85 .output()
86 .map_err(|error| format!("failed to launch git {:?}: {error}", args))?;
87
88 if !output.status.success() {
89 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
90 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
91 let detail = if stderr.is_empty() { stdout } else { stderr };
92 return Err(if detail.is_empty() {
93 format!("git {:?} exited with non-zero status", args)
94 } else {
95 format!("git {:?} failed: {detail}", args)
96 });
97 }
98
99 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
100}