rust_filesearch/fs/
git.rs1#[cfg(feature = "git")]
2use crate::errors::{FsError, Result};
3#[cfg(feature = "git")]
4use crate::models::Entry;
5#[cfg(feature = "git")]
6use std::collections::HashMap;
7#[cfg(feature = "git")]
8use std::path::{Path, PathBuf};
9#[cfg(feature = "git")]
10use std::process::Command;
11
12#[cfg(feature = "git")]
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum GitStatus {
16 Untracked,
17 Modified,
18 Staged,
19 Deleted,
20 Renamed,
21 Unmerged,
22 Ignored,
23 Clean,
24}
25
26#[cfg(feature = "git")]
27impl GitStatus {
28 pub fn from_porcelain_code(code: &str) -> Self {
29 match code {
30 "??" => GitStatus::Untracked,
31 "M " | " M" | "MM" => GitStatus::Modified,
32 "A " | " A" | "AM" => GitStatus::Staged,
33 "D " | " D" => GitStatus::Deleted,
34 "R " | " R" => GitStatus::Renamed,
35 "U " | " U" | "UU" | "AA" | "DD" => GitStatus::Unmerged,
36 "!!" => GitStatus::Ignored,
37 _ => GitStatus::Clean,
38 }
39 }
40
41 pub fn to_str(&self) -> &'static str {
42 match self {
43 GitStatus::Untracked => "untracked",
44 GitStatus::Modified => "modified",
45 GitStatus::Staged => "staged",
46 GitStatus::Deleted => "deleted",
47 GitStatus::Renamed => "renamed",
48 GitStatus::Unmerged => "conflict",
49 GitStatus::Ignored => "ignored",
50 GitStatus::Clean => "clean",
51 }
52 }
53}
54
55#[cfg(feature = "git")]
56#[derive(Debug, Clone)]
58pub struct GitEntry {
59 pub entry: Entry,
60 pub status: GitStatus,
61}
62
63#[cfg(feature = "git")]
64pub fn get_git_status(repo_path: &Path) -> Result<HashMap<PathBuf, GitStatus>> {
66 let output = Command::new("git")
68 .args(["status", "--porcelain", "-uall"])
69 .current_dir(repo_path)
70 .output()
71 .map_err(|e| FsError::IoError {
72 context: "Failed to run git status command".to_string(),
73 source: e,
74 })?;
75
76 if !output.status.success() {
77 return Err(FsError::InvalidFormat {
78 format: format!(
79 "Git command failed: {}",
80 String::from_utf8_lossy(&output.stderr)
81 ),
82 });
83 }
84
85 let mut status_map = HashMap::new();
86 let stdout = String::from_utf8_lossy(&output.stdout);
87
88 for line in stdout.lines() {
89 if line.len() < 4 {
90 continue;
91 }
92
93 let status_code = &line[0..2];
94 let file_path = line[3..].trim();
95
96 let file_path = if let Some(idx) = file_path.find(" -> ") {
98 &file_path[idx + 4..]
99 } else {
100 file_path
101 };
102
103 let status = GitStatus::from_porcelain_code(status_code);
104 let path = repo_path.join(file_path);
105
106 status_map.insert(path, status);
107 }
108
109 Ok(status_map)
110}
111
112#[cfg(feature = "git")]
113pub fn is_git_repo(path: &Path) -> bool {
115 Command::new("git")
116 .args(["rev-parse", "--git-dir"])
117 .current_dir(path)
118 .output()
119 .map(|output| output.status.success())
120 .unwrap_or(false)
121}
122
123#[cfg(feature = "git")]
124pub fn get_changed_since(repo_path: &Path, since_ref: &str) -> Result<Vec<PathBuf>> {
126 let output = Command::new("git")
127 .args(["diff", "--name-only", &format!("{}..HEAD", since_ref)])
128 .current_dir(repo_path)
129 .output()
130 .map_err(|e| FsError::IoError {
131 context: format!("Failed to get git diff since {}", since_ref),
132 source: e,
133 })?;
134
135 if !output.status.success() {
136 return Err(FsError::InvalidFormat {
137 format: format!(
138 "Git diff command failed: {}",
139 String::from_utf8_lossy(&output.stderr)
140 ),
141 });
142 }
143
144 let stdout = String::from_utf8_lossy(&output.stdout);
145 let paths = stdout
146 .lines()
147 .map(|line| repo_path.join(line.trim()))
148 .collect();
149
150 Ok(paths)
151}
152
153#[cfg(feature = "git")]
154pub fn enrich_with_git_status(entries: &[Entry], repo_path: &Path) -> Result<Vec<GitEntry>> {
156 let status_map = get_git_status(repo_path)?;
157
158 let git_entries = entries
159 .iter()
160 .map(|entry| {
161 let status = status_map
162 .get(&entry.path)
163 .copied()
164 .unwrap_or(GitStatus::Clean);
165
166 GitEntry {
167 entry: entry.clone(),
168 status,
169 }
170 })
171 .collect();
172
173 Ok(git_entries)
174}
175
176#[cfg(test)]
177#[cfg(feature = "git")]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_git_status_from_code() {
183 assert_eq!(GitStatus::from_porcelain_code("??"), GitStatus::Untracked);
184 assert_eq!(GitStatus::from_porcelain_code("M "), GitStatus::Modified);
185 assert_eq!(GitStatus::from_porcelain_code(" M"), GitStatus::Modified);
186 assert_eq!(GitStatus::from_porcelain_code("A "), GitStatus::Staged);
187 assert_eq!(GitStatus::from_porcelain_code("D "), GitStatus::Deleted);
188 assert_eq!(GitStatus::from_porcelain_code("UU"), GitStatus::Unmerged);
189 }
190
191 #[test]
192 fn test_git_status_to_str() {
193 assert_eq!(GitStatus::Untracked.to_str(), "untracked");
194 assert_eq!(GitStatus::Modified.to_str(), "modified");
195 assert_eq!(GitStatus::Staged.to_str(), "staged");
196 assert_eq!(GitStatus::Clean.to_str(), "clean");
197 }
198}