Skip to main content

om_context/
git.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5#[derive(Debug)]
6pub enum GitError {
7    NotInstalled,
8    NotARepo,
9    CommandFailed(String),
10}
11
12impl std::fmt::Display for GitError {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        match self {
15            GitError::NotInstalled => write!(f, "git is not installed"),
16            GitError::NotARepo => write!(f, "not a git repository"),
17            GitError::CommandFailed(msg) => write!(f, "git command failed: {}", msg),
18        }
19    }
20}
21
22impl std::error::Error for GitError {}
23
24#[derive(Debug, Default)]
25pub struct GitStatus {
26    pub dirty: HashSet<String>,
27    pub staged: HashSet<String>,
28    pub unstaged: HashSet<String>,
29}
30
31pub fn git_status(root: &Path) -> Result<GitStatus, GitError> {
32    let output = Command::new("git")
33        .arg("status")
34        .arg("--porcelain")
35        .current_dir(root)
36        .output()
37        .map_err(|_| GitError::NotInstalled)?;
38
39    if !output.status.success() {
40        let stderr = String::from_utf8_lossy(&output.stderr);
41        if stderr.contains("not a git repository") {
42            return Err(GitError::NotARepo);
43        }
44        return Err(GitError::CommandFailed(stderr.to_string()));
45    }
46
47    let stdout = String::from_utf8_lossy(&output.stdout);
48    let mut status = GitStatus::default();
49
50    for line in stdout.lines() {
51        if line.len() < 4 {
52            continue;
53        }
54
55        let x = line.chars().next().unwrap();
56        let y = line.chars().nth(1).unwrap();
57        let file = line[3..].trim().to_string();
58
59        let is_staged = x != ' ' && x != '?';
60        let is_unstaged = y != ' ' && y != '?';
61        let is_untracked = x == '?' && y == '?';
62
63        if is_staged {
64            status.staged.insert(file.clone());
65        }
66        if is_unstaged {
67            status.unstaged.insert(file.clone());
68        }
69        if is_staged || is_unstaged || is_untracked {
70            status.dirty.insert(file);
71        }
72    }
73
74    Ok(status)
75}
76
77pub fn ls_files(root: &Path) -> Result<Vec<PathBuf>, GitError> {
78    let output = Command::new("git")
79        .arg("ls-files")
80        .arg("--cached")
81        .arg("--others")
82        .arg("--exclude-standard")
83        .current_dir(root)
84        .output()
85        .map_err(|_| GitError::NotInstalled)?;
86
87    if !output.status.success() {
88        let stderr = String::from_utf8_lossy(&output.stderr);
89        if stderr.contains("not a git repository") {
90            return Err(GitError::NotARepo);
91        }
92        return Err(GitError::CommandFailed(stderr.to_string()));
93    }
94
95    let stdout = String::from_utf8_lossy(&output.stdout);
96    let files = stdout
97        .lines()
98        .map(|line| PathBuf::from(line.trim()))
99        .collect();
100
101    Ok(files)
102}
103
104pub fn repo_root(path: &Path) -> Result<PathBuf, GitError> {
105    let output = Command::new("git")
106        .arg("rev-parse")
107        .arg("--show-toplevel")
108        .current_dir(path)
109        .output()
110        .map_err(|_| GitError::NotInstalled)?;
111
112    if !output.status.success() {
113        let stderr = String::from_utf8_lossy(&output.stderr);
114        if stderr.contains("not a git repository") {
115            return Err(GitError::NotARepo);
116        }
117        return Err(GitError::CommandFailed(stderr.to_string()));
118    }
119
120    let stdout = String::from_utf8_lossy(&output.stdout);
121    let root = PathBuf::from(stdout.trim());
122
123    Ok(root)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::env;
130
131    #[test]
132    fn test_repo_root() {
133        let cwd = env::current_dir().unwrap();
134        let root = repo_root(&cwd);
135        assert!(root.is_ok());
136    }
137
138    #[test]
139    fn test_ls_files() {
140        let cwd = env::current_dir().unwrap();
141        let files = ls_files(&cwd);
142        assert!(files.is_ok());
143    }
144
145    #[test]
146    fn test_git_status() {
147        let cwd = env::current_dir().unwrap();
148        let status = git_status(&cwd);
149        assert!(status.is_ok());
150    }
151}