1use crate::{
4 error::Result,
5 sync::{
6 config::untracked_files_config_repo,
7 repository::{gix_repo, repo},
8 },
9};
10use git2::{Delta, Status, StatusOptions, StatusShow};
11use scopetime::scope_time;
12use std::path::Path;
13
14use super::{RepoPath, ShowUntrackedFilesConfig};
15
16#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
18pub enum StatusItemType {
19 New,
21 Modified,
23 Deleted,
25 Renamed,
27 Typechange,
29 Conflicted,
31}
32
33impl From<gix::status::index_worktree::iter::Summary>
34 for StatusItemType
35{
36 fn from(
37 summary: gix::status::index_worktree::iter::Summary,
38 ) -> Self {
39 use gix::status::index_worktree::iter::Summary;
40
41 match summary {
42 Summary::Removed => Self::Deleted,
43 Summary::Added
44 | Summary::Copied
45 | Summary::IntentToAdd => Self::New,
46 Summary::Modified => Self::Modified,
47 Summary::TypeChange => Self::Typechange,
48 Summary::Renamed => Self::Renamed,
49 Summary::Conflict => Self::Conflicted,
50 }
51 }
52}
53
54impl From<gix::diff::index::ChangeRef<'_, '_>> for StatusItemType {
55 fn from(change_ref: gix::diff::index::ChangeRef) -> Self {
56 use gix::diff::index::ChangeRef;
57
58 match change_ref {
59 ChangeRef::Addition { .. } => Self::New,
60 ChangeRef::Deletion { .. } => Self::Deleted,
61 ChangeRef::Modification { .. }
62 | ChangeRef::Rewrite { .. } => Self::Modified,
63 }
64 }
65}
66
67impl From<Status> for StatusItemType {
68 fn from(s: Status) -> Self {
69 if s.is_index_new() || s.is_wt_new() {
70 Self::New
71 } else if s.is_index_deleted() || s.is_wt_deleted() {
72 Self::Deleted
73 } else if s.is_index_renamed() || s.is_wt_renamed() {
74 Self::Renamed
75 } else if s.is_index_typechange() || s.is_wt_typechange() {
76 Self::Typechange
77 } else if s.is_conflicted() {
78 Self::Conflicted
79 } else {
80 Self::Modified
81 }
82 }
83}
84
85impl From<Delta> for StatusItemType {
86 fn from(d: Delta) -> Self {
87 match d {
88 Delta::Added => Self::New,
89 Delta::Deleted => Self::Deleted,
90 Delta::Renamed => Self::Renamed,
91 Delta::Typechange => Self::Typechange,
92 _ => Self::Modified,
93 }
94 }
95}
96
97#[derive(Clone, Hash, PartialEq, Eq, Debug)]
99pub struct StatusItem {
100 pub path: String,
102 pub status: StatusItemType,
104}
105
106#[derive(Copy, Clone, Default, Hash, PartialEq, Eq, Debug)]
108pub enum StatusType {
109 #[default]
111 WorkingDir,
112 Stage,
114 Both,
116}
117
118impl From<StatusType> for StatusShow {
119 fn from(s: StatusType) -> Self {
120 match s {
121 StatusType::WorkingDir => Self::Workdir,
122 StatusType::Stage => Self::Index,
123 StatusType::Both => Self::IndexAndWorkdir,
124 }
125 }
126}
127
128pub fn is_workdir_clean(
130 repo_path: &RepoPath,
131 show_untracked: Option<ShowUntrackedFilesConfig>,
132) -> Result<bool> {
133 let repo = repo(repo_path)?;
134
135 if repo.is_bare() && !repo.is_worktree() {
136 return Ok(true);
137 }
138
139 let show_untracked = if let Some(config) = show_untracked {
140 config
141 } else {
142 untracked_files_config_repo(&repo)?
143 };
144
145 let mut options = StatusOptions::default();
146 options
147 .show(StatusShow::Workdir)
148 .update_index(true)
149 .include_untracked(show_untracked.include_untracked())
150 .renames_head_to_index(true)
151 .recurse_untracked_dirs(
152 show_untracked.recurse_untracked_dirs(),
153 );
154
155 let statuses = repo.statuses(Some(&mut options))?;
156
157 Ok(statuses.is_empty())
158}
159
160impl From<ShowUntrackedFilesConfig> for gix::status::UntrackedFiles {
161 fn from(value: ShowUntrackedFilesConfig) -> Self {
162 match value {
163 ShowUntrackedFilesConfig::All => Self::Files,
164 ShowUntrackedFilesConfig::Normal => Self::Collapsed,
165 ShowUntrackedFilesConfig::No => Self::None,
166 }
167 }
168}
169
170pub fn get_status(
172 repo_path: &RepoPath,
173 status_type: StatusType,
174 show_untracked: Option<ShowUntrackedFilesConfig>,
175) -> Result<Vec<StatusItem>> {
176 scope_time!("get_status");
177
178 let repo: gix::Repository = gix_repo(repo_path)?;
179
180 let show_untracked = if let Some(config) = show_untracked {
181 config
182 } else {
183 let git2_repo = crate::sync::repository::repo(repo_path)?;
184
185 untracked_files_config_repo(&git2_repo)?
192 };
193
194 let status = repo
195 .status(gix::progress::Discard)?
196 .untracked_files(show_untracked.into());
197
198 let mut res = Vec::new();
199
200 match status_type {
201 StatusType::WorkingDir => {
202 let iter = status.into_index_worktree_iter(Vec::new())?;
203
204 for item in iter {
205 let item = item?;
206
207 let status = item.summary().map(Into::into);
208
209 if let Some(status) = status {
210 let path = item.rela_path().to_string();
211
212 res.push(StatusItem { path, status });
213 }
214 }
215 }
216 StatusType::Stage => {
217 let tree_id: gix::ObjectId =
218 repo.head_tree_id_or_empty()?.into();
219 let worktree_index =
220 gix::worktree::IndexPersistedOrInMemory::Persisted(
221 repo.index_or_empty()?,
222 );
223
224 let mut pathspec = repo.pathspec(
225 false, None::<&str>,
227 true, &gix::index::State::new(repo.object_hash()),
229 gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
230 )?;
231
232 let cb =
233 |change_ref: gix::diff::index::ChangeRef<'_, '_>,
234 _: &gix::index::State,
235 _: &gix::index::State|
236 -> Result<gix::diff::index::Action> {
237 let path = change_ref.fields().0.to_string();
238 let status = change_ref.into();
239
240 res.push(StatusItem { path, status });
241
242 Ok(gix::diff::index::Action::Continue)
243 };
244
245 repo.tree_index_status(
246 &tree_id,
247 &worktree_index,
248 Some(&mut pathspec),
249 gix::status::tree_index::TrackRenames::default(),
250 cb,
251 )?;
252 }
253 StatusType::Both => {
254 let iter = status.into_iter(Vec::new())?;
255
256 for item in iter {
257 let item = item?;
258
259 let path = item.location().to_string();
260
261 let status = match item {
262 gix::status::Item::IndexWorktree(item) => {
263 item.summary().map(Into::into)
264 }
265 gix::status::Item::TreeIndex(change_ref) => {
266 Some(change_ref.into())
267 }
268 };
269
270 if let Some(status) = status {
271 res.push(StatusItem { path, status });
272 }
273 }
274 }
275 }
276
277 res.sort_by(|a, b| {
278 Path::new(a.path.as_str()).cmp(Path::new(b.path.as_str()))
279 });
280
281 Ok(res)
282}
283
284pub fn discard_status(repo_path: &RepoPath) -> Result<bool> {
286 let repo = repo(repo_path)?;
287 let commit = repo.head()?.peel_to_commit()?;
288
289 repo.reset(commit.as_object(), git2::ResetType::Hard, None)?;
290
291 Ok(true)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::{
298 sync::{
299 commit, stage_add_file,
300 status::{get_status, StatusType},
301 tests::{repo_init, repo_init_bare},
302 RepoPath,
303 },
304 StatusItem, StatusItemType,
305 };
306 use std::{fs::File, io::Write, path::Path};
307 use tempfile::TempDir;
308
309 #[test]
310 fn test_discard_status() {
311 let file_path = Path::new("README.md");
312 let (_td, repo) = repo_init().unwrap();
313 let root = repo.path().parent().unwrap();
314 let repo_path: &RepoPath =
315 &root.as_os_str().to_str().unwrap().into();
316
317 let mut file = File::create(root.join(file_path)).unwrap();
318
319 stage_add_file(repo_path, file_path).unwrap();
321 commit(repo_path, "commit msg").unwrap();
322
323 writeln!(file, "Test for discard_status").unwrap();
324
325 let statuses =
326 get_status(repo_path, StatusType::WorkingDir, None)
327 .unwrap();
328 assert_eq!(statuses.len(), 1);
329
330 discard_status(repo_path).unwrap();
331
332 let statuses =
333 get_status(repo_path, StatusType::WorkingDir, None)
334 .unwrap();
335 assert_eq!(statuses.len(), 0);
336 }
337
338 #[test]
339 fn test_get_status_with_workdir() {
340 let (git_dir, _repo) = repo_init_bare().unwrap();
341
342 let separate_workdir = TempDir::new().unwrap();
343
344 let file_path = Path::new("foo");
345 File::create(separate_workdir.path().join(file_path))
346 .unwrap()
347 .write_all(b"a")
348 .unwrap();
349
350 let repo_path = RepoPath::Workdir {
351 gitdir: git_dir.path().into(),
352 workdir: separate_workdir.path().into(),
353 };
354
355 let status =
356 get_status(&repo_path, StatusType::WorkingDir, None)
357 .unwrap();
358
359 assert_eq!(
360 status,
361 vec![StatusItem {
362 path: "foo".into(),
363 status: StatusItemType::New
364 }]
365 );
366 }
367}