git_quick_add/
lib.rs

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
12// Step 1
13/// Gets the file paths of the changes in your repo.
14pub 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        process::exit(1)
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            // 1. Try to get the HEAD → index diff
31            .head_to_index()
32            // If the file differs between HEAD and Index, grab the new file path. (This means the file has been staged.)
33            .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            // 2. Otherwise, try index → workdir diff (This means the file has unstaged changes.)
41            .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                        // 3. If still nothing, try the "old" file's path (maybe a deletion/rename)
53                        // If the file is gone in workdir (deleted) or renamed, take the old file path
54                        .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                        // 4. If nothing worked, fallback to "<unknown>"
64                        .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 the only changes are ignored files, exit
77    if items.is_empty() {
78        println!("{}", console::style("✔ working tree clean ✔").green());
79        process::exit(1)
80    }
81
82    Ok(items)
83}
84
85// Step 2
86/// Prompts the user to select files to stage and returns the selected file paths.
87/// If no files are selected, the program exits.
88/// # Arguments
89/// * `repo` - A reference to the git repository.
90/// # Returns
91/// A vector of selected file paths as strings.
92pub fn choose_files(path_items: Vec<PathItems>) -> Vec<PathItems> {
93    // TODO: Include the status of each file in the prompt (e.g., "M", "A", "D", "??")
94    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")
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    let mut paths: Vec<PathItems> = path_items.clone();
108
109    for index in selections {
110        paths[index].is_selected = true;
111    }
112
113    paths
114}
115
116// Step 3
117/// Stages the selected files in the git repository.
118/// If staging fails, the program exits.
119/// # Arguments
120/// * `repo` - A reference to the git repository.
121/// * `paths` - A vector of file paths to stage.
122pub fn git_add_selected(repo: &Repository, paths: &Vec<PathItems>) -> Result<(), git2::Error> {
123    let mut index = repo.index()?;
124
125    println!("{}", console::style("Changes Made:").bold());
126
127    let mut logs = vec![];
128
129    for item in paths {
130        // if the item is staged and not selected, we need to unstage it
131        if item.is_staged && !item.is_selected {
132            let target = repo.head()?.peel(git2::ObjectType::Commit)?;
133            repo.reset_default(Some(&target), &[&item.path])?;
134
135            logs.push(format!(
136                " - {} {}",
137                console::style("Unstaged:").yellow(),
138                item.path.clone()
139            ));
140        } else if !item.is_staged && item.is_selected {
141            let p = Path::new(&item.path);
142
143            index.add_path(p).unwrap_or_else(|e| {
144                eprintln!("{}", e);
145                process::exit(1)
146            });
147
148            logs.push(format!(
149                " - {} {}",
150                console::style("Staged:").green(),
151                item.path
152            ));
153
154            index.write().unwrap_or_else(|_| {
155                println!("{}", console::style("Failed to write index").red());
156                process::exit(1)
157            });
158        } else {
159            if item.is_staged {
160                logs.push(format!(
161                    " - {} {}",
162                    console::style("Staged:").green(),
163                    item.path.clone()
164                ));
165            } else {
166                logs.push(format!(
167                    " - {} {}",
168                    console::style("Unstaged:").yellow(),
169                    item.path.clone()
170                ));
171            }
172        }
173    }
174
175    println!("{}", logs.join("\n"));
176
177    Ok(())
178}
179
180// Tests
181#[cfg(test)]
182mod tests {}