Skip to main content

agentctl/debug/sources/
git_context.rs

1use 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}