Skip to main content

hematite/agent/
git.rs

1use std::io;
2use std::path::Path;
3use std::process::{Command, Stdio};
4
5pub fn is_git_repo(path: &Path) -> bool {
6    Command::new("git")
7        .arg("-C")
8        .arg(path)
9        .arg("rev-parse")
10        .arg("--is-inside-work-tree")
11        .stdout(Stdio::null())
12        .stderr(Stdio::null())
13        .status()
14        .map(|s| s.success())
15        .unwrap_or(false)
16}
17
18/// Takes a "Ghost Snapshot" by saving the current state to a hidden ref.
19/// This allows for easy rollbacks without creating visible commits or branches.
20pub fn create_ghost_snapshot(repo_path: &Path) -> io::Result<()> {
21    // 1. Stage all changes (so we don't lose the original state of the file we're about to edit)
22    let add_status = Command::new("git")
23        .arg("-C")
24        .arg(repo_path)
25        .arg("add")
26        .arg("-A")
27        .stdout(Stdio::null())
28        .stderr(Stdio::null())
29        .status()?;
30
31    if !add_status.success() {
32        return Err(io::Error::new(io::ErrorKind::Other, "Git add failed"));
33    }
34
35    // 2. Create a tree from the index
36    let tree_output = Command::new("git")
37        .arg("-C")
38        .arg(repo_path)
39        .arg("write-tree")
40        .stderr(Stdio::null())
41        .output()?;
42
43    if !tree_output.status.success() {
44        return Err(io::Error::new(
45            io::ErrorKind::Other,
46            "Git write-tree failed",
47        ));
48    }
49    let tree_sha = String::from_utf8_lossy(&tree_output.stdout)
50        .trim()
51        .to_string();
52
53    // 3. Create a commit object (parent is HEAD)
54    let commit_output = Command::new("git")
55        .arg("-C")
56        .arg(repo_path)
57        .arg("commit-tree")
58        .arg(&tree_sha)
59        .arg("-p")
60        .arg("HEAD")
61        .arg("-m")
62        .arg("Hematite Ghost Snapshot")
63        .stderr(Stdio::null())
64        .output()?;
65
66    if !commit_output.status.success() {
67        return Err(io::Error::new(
68            io::ErrorKind::Other,
69            "Git commit-tree failed",
70        ));
71    }
72    let commit_sha = String::from_utf8_lossy(&commit_output.stdout)
73        .trim()
74        .to_string();
75
76    // 4. Update the hidden ghost ref
77    let update_status = Command::new("git")
78        .arg("-C")
79        .arg(repo_path)
80        .arg("update-ref")
81        .arg("refs/hematite/ghost")
82        .arg(&commit_sha)
83        .stdout(Stdio::null())
84        .stderr(Stdio::null())
85        .status()?;
86
87    if !update_status.success() {
88        return Err(io::Error::new(
89            io::ErrorKind::Other,
90            "Git update-ref failed",
91        ));
92    }
93
94    Ok(())
95}
96
97/// Reverts a file to its state in the last Ghost Snapshot.
98pub fn revert_from_ghost(repo_path: &Path, file_path: &str) -> io::Result<String> {
99    let status = Command::new("git")
100        .arg("-C")
101        .arg(repo_path)
102        .arg("checkout")
103        .arg("refs/hematite/ghost")
104        .arg("--")
105        .arg(file_path)
106        .stdout(Stdio::null())
107        .stderr(Stdio::null())
108        .status()?;
109
110    if !status.success() {
111        return Err(io::Error::new(
112            io::ErrorKind::Other,
113            "Git checkout from ghost ref failed",
114        ));
115    }
116
117    Ok(format!("Restored {} from Git Ghost ref", file_path))
118}
119
120pub fn get_active_branch(repo_path: &Path) -> io::Result<String> {
121    let output = Command::new("git")
122        .arg("-C")
123        .arg(repo_path)
124        .arg("rev-parse")
125        .arg("--abbrev-ref")
126        .arg("HEAD")
127        .stderr(Stdio::null())
128        .output()?;
129    if !output.status.success() {
130        return Err(io::Error::new(io::ErrorKind::Other, "Git rev-parse failed"));
131    }
132    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
133}