cersei_tools/
git_utils.rs1use std::path::{Path, PathBuf};
6use std::process::Command;
7
8pub fn is_git_repo(path: &Path) -> bool {
10 Command::new("git")
11 .args(["rev-parse", "--is-inside-work-tree"])
12 .current_dir(path)
13 .output()
14 .map(|o| o.status.success())
15 .unwrap_or(false)
16}
17
18pub fn get_repo_root(path: &Path) -> Option<PathBuf> {
20 Command::new("git")
21 .args(["rev-parse", "--show-toplevel"])
22 .current_dir(path)
23 .output()
24 .ok()
25 .and_then(|o| {
26 if o.status.success() {
27 Some(PathBuf::from(String::from_utf8_lossy(&o.stdout).trim()))
28 } else {
29 None
30 }
31 })
32}
33
34pub fn current_branch(path: &Path) -> Option<String> {
36 Command::new("git")
37 .args(["rev-parse", "--abbrev-ref", "HEAD"])
38 .current_dir(path)
39 .output()
40 .ok()
41 .and_then(|o| {
42 if o.status.success() {
43 Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
44 } else {
45 None
46 }
47 })
48}
49
50pub fn git_status(path: &Path) -> Option<String> {
52 Command::new("git")
53 .args(["status", "--short"])
54 .current_dir(path)
55 .output()
56 .ok()
57 .and_then(|o| {
58 if o.status.success() {
59 Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
60 } else {
61 None
62 }
63 })
64}
65
66pub fn git_diff(path: &Path) -> Option<String> {
68 let staged = Command::new("git")
69 .args(["diff", "--cached", "--stat"])
70 .current_dir(path)
71 .output()
72 .ok()
73 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
74 .unwrap_or_default();
75
76 let unstaged = Command::new("git")
77 .args(["diff", "--stat"])
78 .current_dir(path)
79 .output()
80 .ok()
81 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
82 .unwrap_or_default();
83
84 let combined = format!("{}\n{}", staged, unstaged).trim().to_string();
85 if combined.is_empty() {
86 None
87 } else {
88 Some(combined)
89 }
90}
91
92pub fn recent_commits(path: &Path, count: usize) -> Option<String> {
94 Command::new("git")
95 .args(["log", "--oneline", &format!("-{}", count)])
96 .current_dir(path)
97 .output()
98 .ok()
99 .and_then(|o| {
100 if o.status.success() {
101 Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
102 } else {
103 None
104 }
105 })
106}
107
108pub fn list_modified_files(path: &Path) -> Vec<String> {
110 Command::new("git")
111 .args(["diff", "--name-only", "HEAD"])
112 .current_dir(path)
113 .output()
114 .ok()
115 .map(|o| {
116 String::from_utf8_lossy(&o.stdout)
117 .lines()
118 .filter(|l| !l.is_empty())
119 .map(String::from)
120 .collect()
121 })
122 .unwrap_or_default()
123}
124
125pub fn build_git_context(working_dir: &Path) -> Option<String> {
127 if !is_git_repo(working_dir) {
128 return None;
129 }
130
131 let mut parts = Vec::new();
132
133 if let Some(branch) = current_branch(working_dir) {
134 parts.push(format!("Current branch: {}", branch));
135 }
136
137 if let Some(status) = git_status(working_dir) {
138 if !status.is_empty() {
139 parts.push(format!("Status:\n{}", status));
140 } else {
141 parts.push("Status: (clean)".to_string());
142 }
143 }
144
145 if let Some(commits) = recent_commits(working_dir, 5) {
146 parts.push(format!("Recent commits:\n{}", commits));
147 }
148
149 if parts.is_empty() {
150 None
151 } else {
152 Some(parts.join("\n\n"))
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_is_git_repo() {
162 let cwd = std::env::current_dir().unwrap();
164 let _ = is_git_repo(&cwd); }
167
168 #[test]
169 fn test_not_git_repo() {
170 let tmp = tempfile::tempdir().unwrap();
171 assert!(!is_git_repo(tmp.path()));
172 }
173
174 #[test]
175 fn test_build_git_context_non_repo() {
176 let tmp = tempfile::tempdir().unwrap();
177 assert!(build_git_context(tmp.path()).is_none());
178 }
179
180 #[test]
181 fn test_git_context_real_repo() {
182 let root = Path::new(env!("CARGO_MANIFEST_DIR"))
184 .parent()
185 .unwrap()
186 .parent()
187 .unwrap();
188 if is_git_repo(root) {
189 let ctx = build_git_context(root);
190 assert!(ctx.is_some());
191 let ctx = ctx.unwrap();
192 assert!(ctx.contains("branch") || ctx.contains("Status"));
193 }
194 }
195}