eazygit/commands/git_commands/
stage.rs1use crate::commands::{Command, CommandResult};
7use crate::services::GitService;
8use crate::app::{AppState, Action, reducer};
9use crate::errors::CommandError;
10use tracing::instrument;
11
12pub struct StageFilesCommand {
14 pub paths: Vec<String>,
15}
16
17impl Command for StageFilesCommand {
18 #[instrument(skip(self, git, state), fields(paths = ?self.paths))]
19 fn execute(
20 &self,
21 git: &GitService,
22 state: &AppState,
23 ) -> Result<CommandResult, CommandError> {
24 let mut staged_count = 0;
25 let mut errors = Vec::new();
26
27 for path in &self.paths {
28 match git.stage_file(&state.repo_path, path) {
29 Ok(_) => staged_count += 1,
30 Err(e) => errors.push(format!("stage error: {e}")),
31 }
32 }
33
34 let mut new_state = state.clone();
35
36 if !errors.is_empty() {
37 new_state = reducer(new_state, Action::SetStatusError(Some(errors.join(", "))));
38 }
39
40 if staged_count > 0 {
41 for entry in new_state.status_entries.iter_mut() {
44 if self.paths.contains(&entry.path) {
45 entry.staged = true;
46 }
48 }
49
50 new_state = reducer(new_state, Action::SetFeedback(Some(
51 format!("Staged {} file(s)", staged_count)
52 )));
53 new_state = reducer(new_state, Action::ClearFileSelects);
54 new_state = reducer(new_state, Action::SetRefreshing(true));
55 }
56
57 Ok(CommandResult::StateUpdate(new_state))
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::commands::Command as CommandTrait;
65 use tempfile::tempdir;
66 use std::fs;
67 use std::process::Command;
68
69 fn setup_test_repo() -> (std::path::PathBuf, GitService) {
70 let temp_dir = tempdir().expect("Failed to create temp directory");
71 let repo_path = temp_dir.path().to_path_buf();
72
73 Command::new("git")
74 .arg("init")
75 .arg(&repo_path)
76 .output()
77 .expect("Failed to init repo");
78
79 Command::new("git")
80 .args(["-C", repo_path.to_str().unwrap(), "config", "user.name", "Test"])
81 .output()
82 .expect("Failed to set user.name");
83
84 Command::new("git")
85 .args(["-C", repo_path.to_str().unwrap(), "config", "user.email", "test@test.com"])
86 .output()
87 .expect("Failed to set user.email");
88
89 let test_file = repo_path.join("existing.txt");
91 fs::write(&test_file, "content").expect("Failed to write");
92 Command::new("git")
93 .args(["-C", repo_path.to_str().unwrap(), "add", "existing.txt"])
94 .output()
95 .expect("Failed to stage");
96 Command::new("git")
97 .args(["-C", repo_path.to_str().unwrap(), "commit", "-m", "Initial"])
98 .output()
99 .expect("Failed to commit");
100
101 std::mem::forget(temp_dir);
102 let git_service = GitService::new();
103 (repo_path, git_service)
104 }
105
106 #[test]
107 fn test_stage_files_command_structure() {
108 let command = StageFilesCommand {
109 paths: vec!["test.txt".to_string()],
110 };
111 assert_eq!(command.paths.len(), 1);
112 }
113
114 #[test]
115 fn test_stage_files_execution() {
116 let (repo_path, git_service) = setup_test_repo();
117 let mut state = AppState::new();
118 state.repo_path = repo_path.to_str().unwrap().to_string();
119
120 let test_file = repo_path.join("test.txt");
122 fs::write(&test_file, "content").expect("Failed to write");
123
124 let command = StageFilesCommand {
125 paths: vec!["test.txt".to_string()],
126 };
127
128 let result = CommandTrait::execute(&command, &git_service, &state);
129 assert!(result.is_ok());
130
131 let status = git_service.status_porcelain(repo_path.to_str().unwrap())
133 .expect("Failed to get status");
134 assert!(status.contains("A ") || status.contains("test.txt"));
135
136 let _ = fs::remove_dir_all(&repo_path);
137 }
138
139 #[test]
140 fn test_unstage_files_execution() {
141 let (repo_path, git_service) = setup_test_repo();
142 let mut state = AppState::new();
143 state.repo_path = repo_path.to_str().unwrap().to_string();
144
145 let test_file = repo_path.join("test.txt");
147 fs::write(&test_file, "content").expect("Failed to write");
148 git_service.stage_file(repo_path.to_str().unwrap(), "test.txt")
149 .expect("Failed to stage");
150
151 let command = UnstageFilesCommand {
152 paths: vec!["test.txt".to_string()],
153 };
154
155 let result = CommandTrait::execute(&command, &git_service, &state);
156 assert!(result.is_ok());
157
158 let status = git_service.status_porcelain(repo_path.to_str().unwrap())
160 .expect("Failed to get status");
161 assert!(!status.contains("A "));
162
163 let _ = fs::remove_dir_all(&repo_path);
164 }
165
166 #[test]
167 fn test_command_error_handling() {
168 let (repo_path, git_service) = setup_test_repo();
169 let mut state = AppState::new();
170 state.repo_path = repo_path.to_str().unwrap().to_string();
171
172 let command = StageFilesCommand {
174 paths: vec!["nonexistent.txt".to_string()],
175 };
176
177 let result = CommandTrait::execute(&command, &git_service, &state);
179 assert!(result.is_ok() || result.is_err());
181
182 let _ = fs::remove_dir_all(&repo_path);
183 }
184}
185
186pub struct UnstageFilesCommand {
188 pub paths: Vec<String>,
189}
190
191impl Command for UnstageFilesCommand {
192 #[instrument(skip(self, git, state), fields(paths = ?self.paths))]
193 fn execute(
194 &self,
195 git: &GitService,
196 state: &AppState,
197 ) -> Result<CommandResult, CommandError> {
198 let mut unstaged_count = 0;
199 let mut errors = Vec::new();
200
201 for path in &self.paths {
202 match git.unstage_file(&state.repo_path, path) {
203 Ok(_) => unstaged_count += 1,
204 Err(e) => errors.push(format!("unstage error: {e}")),
205 }
206 }
207
208 let mut new_state = state.clone();
209
210 if !errors.is_empty() {
211 new_state = reducer(new_state, Action::SetStatusError(Some(errors.join(", "))));
212 }
213
214 if unstaged_count > 0 {
215 for entry in new_state.status_entries.iter_mut() {
218 if self.paths.contains(&entry.path) {
219 entry.staged = false;
220 }
222 }
223
224 new_state = reducer(new_state, Action::SetFeedback(Some(
225 format!("Unstaged {} file(s)", unstaged_count)
226 )));
227 new_state = reducer(new_state, Action::ClearFileSelects);
228 new_state = reducer(new_state, Action::SetRefreshing(true));
229 }
230
231 Ok(CommandResult::StateUpdate(new_state))
232 }
233}
234