1use dialoguer::MultiSelect;
2use git2::{Repository, Status};
3use std::{path::Path, process};
4
5#[derive(Clone, Debug)]
6pub struct PathItems {
7 path: String,
8 is_staged: bool,
9 is_selected: bool,
10}
11
12pub fn get_paths(repo: &Repository) -> Result<Vec<PathItems>, git2::Error> {
15 let statuses = repo.statuses(None)?;
16
17 if statuses.is_empty() {
18 println!("{}", console::style("✔ working tree clean ✔").green());
19 return Ok(vec![]);
20 }
21
22 let mut items: Vec<PathItems> = vec![];
23
24 for diff_entry in statuses.iter() {
25 if diff_entry.status() == Status::IGNORED {
26 continue;
27 }
28
29 let path_items = diff_entry
30 .head_to_index()
32 .and_then(|d| {
34 Some(PathItems {
35 path: String::from(d.new_file().path()?.display().to_string()),
36 is_staged: true,
37 is_selected: false,
38 })
39 })
40 .or_else(|| {
42 Option::from(
43 diff_entry
44 .index_to_workdir()
45 .and_then(|d| {
46 Some(PathItems {
47 path: String::from(d.new_file().path()?.display().to_string()),
48 is_staged: false,
49 is_selected: false,
50 })
51 })
52 .or_else(|| {
55 diff_entry.index_to_workdir().and_then(|d| {
56 Some(PathItems {
57 path: String::from(d.old_file().path()?.display().to_string()),
58 is_staged: false,
59 is_selected: false,
60 })
61 })
62 })
63 .unwrap_or_else(|| PathItems {
65 path: String::from("<unknown>"),
66 is_staged: false,
67 is_selected: false,
68 }),
69 )
70 })
71 .unwrap();
72
73 items.push(path_items);
74 }
75
76 if items.is_empty() {
78 println!("{}", console::style("✔ working tree clean ✔").green());
79 process::exit(1)
80 }
81
82 Ok(items)
83}
84
85pub fn choose_files(mut path_items: Vec<PathItems>) -> Vec<PathItems> {
93 let list_of_paths: Vec<String> = path_items.iter().map(|p| p.path.clone()).collect();
95 let list_of_preselected: Vec<bool> = path_items.iter().map(|p| p.is_staged).collect();
96
97 let selections = MultiSelect::new()
98 .with_prompt("Choose files to stage - (use Space to select - press Enter to submit)")
99 .items(list_of_paths)
100 .defaults(&list_of_preselected)
101 .interact()
102 .unwrap_or_else(|_| {
103 eprintln!("{}", console::style("Error selecting files").red());
104 process::exit(1)
105 });
106
107 for index in selections {
108 path_items[index].is_selected = true;
109 }
110
111 path_items
112}
113
114pub fn git_add_selected(repo: &Repository, paths: &Vec<PathItems>) -> Result<(), git2::Error> {
121 let mut index = repo.index()?;
122
123 println!("{}", console::style("Changes Made:").bold());
124
125 let mut logs = vec![];
126
127 for item in paths {
128 if item.is_staged && !item.is_selected {
130 let target = repo.head()?.peel(git2::ObjectType::Commit)?;
131 repo.reset_default(Some(&target), &[&item.path])?;
132
133 logs.push(format!(
134 " - {} {}",
135 console::style("Unstaged:").yellow(),
136 item.path.clone()
137 ));
138 } else if !item.is_staged && item.is_selected {
139 let p = Path::new(&item.path);
140
141 index.add_path(p).unwrap_or_else(|error| {
142 eprintln!(
143 "{} {} - {}",
144 console::style("Failed to add path for").red(),
145 console::style(&item.path).yellow(),
146 error
147 );
148 });
149
150 logs.push(format!(
151 " - {} {}",
152 console::style("Staged:").green(),
153 item.path
154 ));
155
156 index.write().unwrap_or_else(|error| {
157 eprintln!(
158 "{} {} - {}",
159 console::style("Failed to write index for").red(),
160 console::style(&item.path).yellow(),
161 error
162 );
163 });
164 } else {
165 if item.is_staged {
166 logs.push(format!(
167 " - {} {}",
168 console::style("Staged:").green(),
169 item.path.clone()
170 ));
171 } else {
172 logs.push(format!(
173 " - {} {}",
174 console::style("Unstaged:").yellow(),
175 item.path.clone()
176 ));
177 }
178 }
179 }
180
181 println!("{}", logs.join("\n"));
182
183 Ok(())
184}
185
186#[cfg(test)]
188mod tests {
189 use super::*;
190 use git2::{Oid, Repository, Signature};
191 use std::fs::File;
192 use std::io::Write;
193 use tempfile::TempDir;
194
195 fn init_repo() -> (TempDir, Repository) {
197 let tmp_dir = TempDir::new().expect("create temp dir");
198 let repo = Repository::init(tmp_dir.path()).expect("init repo");
199 (tmp_dir, repo)
200 }
201
202 fn commit_file(repo: &Repository, file_path: &str, content: &str, message: &str) -> Oid {
204 let mut file = File::create(repo.workdir().unwrap().join(file_path)).unwrap();
205 file.write_all(content.as_bytes()).unwrap();
206
207 let mut index = repo.index().unwrap();
208 index.add_path(Path::new(file_path)).unwrap();
209 let oid = index.write_tree().unwrap();
210
211 let sig = Signature::now("Test", "test@example.com").unwrap();
212 let tree = repo.find_tree(oid).unwrap();
213
214 let parent_commit = repo
215 .head()
216 .ok()
217 .and_then(|h| h.target())
218 .and_then(|oid| repo.find_commit(oid).ok());
219
220 let commit_oid = if let Some(parent) = parent_commit {
221 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])
222 .unwrap()
223 } else {
224 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
225 .unwrap()
226 };
227 commit_oid
228 }
229
230 #[test]
231 fn test_get_paths_empty_worktree() {
232 let (_tmp, repo) = init_repo();
233
234 let statuses = get_paths(&repo).unwrap();
236 assert!(statuses.is_empty());
237 }
238
239 #[test]
240 fn test_get_paths_unstaged_file() {
241 let (_tmp, repo) = init_repo();
242
243 let file_path = "foo.txt";
245 let file_full_path = repo.workdir().unwrap().join(file_path);
246 let mut file = File::create(&file_full_path).unwrap();
247 writeln!(file, "hello world").unwrap();
248
249 let paths = get_paths(&repo).unwrap();
251 assert_eq!(paths.len(), 1);
252 let item = &paths[0];
253 assert_eq!(item.path, file_path);
254 assert!(!item.is_staged);
255 assert!(!item.is_selected);
256 }
257
258 #[test]
259 fn test_get_paths_staged_file() {
260 let (_tmp, repo) = init_repo();
261
262 let file_path = "bar.txt";
264 let file_full_path = repo.workdir().unwrap().join(file_path);
265 let mut file = File::create(&file_full_path).unwrap();
266 writeln!(file, "hello staged").unwrap();
267
268 let mut index = repo.index().unwrap();
269 index.add_path(Path::new(file_path)).unwrap();
270 index.write().unwrap();
271
272 let paths = get_paths(&repo).unwrap();
274 assert_eq!(paths.len(), 1);
275 let item = &paths[0];
276 assert_eq!(item.path, file_path);
277 assert!(item.is_staged);
278 assert!(!item.is_selected);
279 }
280
281 #[test]
282 fn test_get_paths_staged_and_unstaged() {
283 let (_tmp, repo) = init_repo();
284
285 commit_file(&repo, "init.txt", "init", "init commit");
287
288 let staged_path = "staged.txt";
290 let staged_full_path = repo.workdir().unwrap().join(staged_path);
291 let mut staged_file = File::create(&staged_full_path).unwrap();
292 writeln!(staged_file, "staged content").unwrap();
293
294 let mut index = repo.index().unwrap();
295 index.add_path(Path::new(staged_path)).unwrap();
296 index.write().unwrap();
297
298 let unstaged_path = "unstaged.txt";
300 let unstaged_full_path = repo.workdir().unwrap().join(unstaged_path);
301 let mut unstaged_file = File::create(&unstaged_full_path).unwrap();
302 writeln!(unstaged_file, "unstaged content").unwrap();
303
304 let mut paths = get_paths(&repo).unwrap();
306 paths.sort_by(|a, b| a.path.cmp(&b.path));
307 assert_eq!(paths.len(), 2);
308
309 let staged = paths.iter().find(|p| p.path == staged_path).unwrap();
310 assert!(staged.is_staged);
311
312 let unstaged = paths.iter().find(|p| p.path == unstaged_path).unwrap();
313 assert!(!unstaged.is_staged);
314 }
315}