Skip to main content

git_quick_add/git/
status.rs

1use crate::models::path_items::PathItems;
2use git2::{Repository, Status};
3use std::process;
4
5/// Gets the file paths of the changes in your repo.
6pub fn get_paths(repo: &Repository) -> Result<Vec<PathItems>, git2::Error> {
7    let statuses = repo.statuses(None)?;
8
9    if statuses.is_empty() {
10        println!("{}", console::style("✔ working tree clean ✔").green());
11        return Ok(vec![]);
12    }
13
14    let mut items: Vec<PathItems> = vec![];
15
16    for diff_entry in statuses.iter() {
17        if diff_entry.status() == Status::IGNORED {
18            continue;
19        }
20
21        // Skip swap files (e.g., Vim creates .swp files that we don't want to show)
22        if diff_entry.path().unwrap().ends_with(".swp") {
23            continue;
24        }
25
26        let path_items = diff_entry
27            // 1. Try to get the HEAD → index diff
28            .head_to_index()
29            // If the file differs between HEAD and Index, grab the new file path. (This means the file has been staged.)
30            .and_then(|d| {
31                Some(PathItems {
32                    path: String::from(d.new_file().path()?.display().to_string()),
33                    is_staged: true,
34                    is_selected: false,
35                })
36            })
37            // 2. Otherwise, try index → workdir diff (This means the file has unstaged changes.)
38            .or_else(|| {
39                Option::from(
40                    diff_entry
41                        .index_to_workdir()
42                        .and_then(|d| {
43                            Some(PathItems {
44                                path: String::from(d.new_file().path()?.display().to_string()),
45                                is_staged: false,
46                                is_selected: false,
47                            })
48                        })
49                        // 3. If still nothing, try the "old" file's path (maybe a deletion/rename)
50                        // If the file is gone in workdir (deleted) or renamed, take the old file path
51                        .or_else(|| {
52                            diff_entry.index_to_workdir().and_then(|d| {
53                                Some(PathItems {
54                                    path: String::from(d.old_file().path()?.display().to_string()),
55                                    is_staged: false,
56                                    is_selected: false,
57                                })
58                            })
59                        })
60                        // 4. If nothing worked, fallback to "<unknown>"
61                        .unwrap_or_else(|| PathItems {
62                            path: String::from("<unknown>"),
63                            is_staged: false,
64                            is_selected: false,
65                        }),
66                )
67            })
68            .unwrap();
69
70        items.push(path_items);
71    }
72
73    // If the only changes are ignored files, exit
74    if items.is_empty() {
75        println!("{}", console::style("✔ working tree clean ✔").green());
76        process::exit(1)
77    }
78
79    Ok(items)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use git2::{Oid, Repository, Signature};
86    use std::fs::File;
87    use std::io::Write;
88    use std::path::Path;
89    use tempfile::TempDir;
90
91    /// Helper to initialize a new git repository in a temp dir
92    fn init_repo() -> (TempDir, Repository) {
93        let tmp_dir = TempDir::new().expect("create temp dir");
94        let repo = Repository::init(tmp_dir.path()).expect("init repo");
95        (tmp_dir, repo)
96    }
97
98    /// Helper to commit a file to the repo
99    fn commit_file(repo: &Repository, file_path: &str, content: &str, message: &str) -> Oid {
100        let mut file = File::create(repo.workdir().unwrap().join(file_path)).unwrap();
101        file.write_all(content.as_bytes()).unwrap();
102
103        let mut index = repo.index().unwrap();
104        index.add_path(Path::new(file_path)).unwrap();
105        let oid = index.write_tree().unwrap();
106
107        let sig = Signature::now("Test", "test@example.com").unwrap();
108        let tree = repo.find_tree(oid).unwrap();
109
110        let parent_commit = repo
111            .head()
112            .ok()
113            .and_then(|h| h.target())
114            .and_then(|oid| repo.find_commit(oid).ok());
115
116        let commit_oid = if let Some(parent) = parent_commit {
117            repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])
118                .unwrap()
119        } else {
120            repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
121                .unwrap()
122        };
123        commit_oid
124    }
125
126    #[test]
127    fn test_get_paths_empty_worktree() {
128        let (_tmp, repo) = init_repo();
129
130        // No files, clean worktree
131        let statuses = get_paths(&repo).unwrap();
132        assert!(statuses.is_empty());
133    }
134
135    #[test]
136    fn test_get_paths_unstaged_file() {
137        let (_tmp, repo) = init_repo();
138
139        // Create a file but do not stage it
140        let file_path = "foo.txt";
141        let file_full_path = repo.workdir().unwrap().join(file_path);
142        let mut file = File::create(&file_full_path).unwrap();
143        writeln!(file, "hello world").unwrap();
144
145        // Now, get_paths should return one PathItems with is_staged == false
146        let paths = get_paths(&repo).unwrap();
147        assert_eq!(paths.len(), 1);
148        let item = &paths[0];
149        assert_eq!(item.path, file_path);
150        assert!(!item.is_staged);
151        assert!(!item.is_selected);
152    }
153
154    #[test]
155    fn test_get_paths_staged_file() {
156        let (_tmp, repo) = init_repo();
157
158        // Create and stage a file
159        let file_path = "bar.txt";
160        let file_full_path = repo.workdir().unwrap().join(file_path);
161        let mut file = File::create(&file_full_path).unwrap();
162        writeln!(file, "hello staged").unwrap();
163
164        let mut index = repo.index().unwrap();
165        index.add_path(Path::new(file_path)).unwrap();
166        index.write().unwrap();
167
168        // Now, get_paths should return one PathItems with is_staged == true
169        let paths = get_paths(&repo).unwrap();
170        assert_eq!(paths.len(), 1);
171        let item = &paths[0];
172        assert_eq!(item.path, file_path);
173        assert!(item.is_staged);
174        assert!(!item.is_selected);
175    }
176
177    #[test]
178    fn test_get_paths_staged_and_unstaged() {
179        let (_tmp, repo) = init_repo();
180
181        // Commit an initial file
182        commit_file(&repo, "init.txt", "init", "init commit");
183
184        // Add and stage a file
185        let staged_path = "staged.txt";
186        let staged_full_path = repo.workdir().unwrap().join(staged_path);
187        let mut staged_file = File::create(&staged_full_path).unwrap();
188        writeln!(staged_file, "staged content").unwrap();
189
190        let mut index = repo.index().unwrap();
191        index.add_path(Path::new(staged_path)).unwrap();
192        index.write().unwrap();
193
194        // Add an unstaged file
195        let unstaged_path = "unstaged.txt";
196        let unstaged_full_path = repo.workdir().unwrap().join(unstaged_path);
197        let mut unstaged_file = File::create(&unstaged_full_path).unwrap();
198        writeln!(unstaged_file, "unstaged content").unwrap();
199
200        // Now, get_paths should return two PathItems
201        let mut paths = get_paths(&repo).unwrap();
202        paths.sort_by(|a, b| a.path.cmp(&b.path));
203        assert_eq!(paths.len(), 2);
204
205        let staged = paths.iter().find(|p| p.path == staged_path).unwrap();
206        assert!(staged.is_staged);
207
208        let unstaged = paths.iter().find(|p| p.path == unstaged_path).unwrap();
209        assert!(!unstaged.is_staged);
210    }
211}