1use crate::repo_read_index::{IndexEntry, parse_index};
19use crate::repo_read_layout::ObjectFormat;
20use crate::repo_read_odb::ObjectDatabase;
21use crate::repo_read_refs::{Refs, RefValue};
22use crate::repo_read_tree::{FlatEntry, MODE_GITLINK, flatten_tree, parse_commit};
23use crate::fs_beneath::{entry_metadata_at, open_directory_beneath, open_root, plain_components};
24use crate::worktree_clean_content::{ContentJudge, ConversionEnvironment, Place};
25use crate::subvolume_index::{StatFields, StoredStat, be32, walk_entries};
26use crate::subvolume_index_files::{fields_of, matches_stored, repository_hash_length};
27use crate::transaction_git::{GitContext, GitRequest, run_git};
28use std::ffi::OsStr;
29use std::os::unix::ffi::OsStrExt;
30use std::os::unix::fs::MetadataExt;
31use std::path::Path;
32
33const IN_PROGRESS: &[&str] =
35 &["index.lock", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "BISECT_LOG", "rebase-merge", "rebase-apply", "sequencer"];
36
37const ASSUME_VALID: u16 = 0x8000;
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum CleanRefusalKind {
53 OperationInProgress(&'static str),
55 Unreadable,
57 NotPlain,
60 StagedChange,
62 FileChanged,
64 StatStale,
66 Racy,
68 Untracked,
70}
71
72impl CleanRefusalKind {
73 pub fn restorable(self) -> bool {
76 return matches!(self, Self::StagedChange | Self::FileChanged | Self::StatStale | Self::Racy | Self::Untracked);
77 }
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct CleanRefusal {
91 pub kind: CleanRefusalKind,
93 pub reason: String,
95}
96
97impl CleanRefusal {
98 fn new(kind: CleanRefusalKind, reason: impl Into<String>) -> CleanRefusal {
100 return CleanRefusal { kind, reason: reason.into() };
101 }
102
103 fn unreadable(reason: impl Into<String>) -> CleanRefusal {
105 return CleanRefusal::new(CleanRefusalKind::Unreadable, reason);
106 }
107}
108
109impl std::fmt::Display for CleanRefusal {
110 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 return formatter.write_str(self.reason.as_str());
112 }
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct StatEntry {
126 pub path: Vec<u8>,
128 pub flags: u16,
130 pub mode: u32,
132 pub stored: StoredStat,
134}
135
136#[derive(Clone, Debug, Eq, PartialEq)]
146pub struct CleanSource {
147 pub head_commit: String,
149 pub head_tree: Vec<u8>,
151 pub tracked: usize,
153 pub index_seconds: u32,
155 pub hash_length: usize,
157 pub attribute_blobs: Vec<(Vec<u8>, Vec<u8>)>,
159 pub index_bytes: Vec<u8>,
162}
163
164fn operation_in_progress(git_dir: &Path, owned_index_lock: bool) -> Option<&'static str> {
175 return IN_PROGRESS.iter().copied().filter(|name: &&str| { return !(owned_index_lock && *name == "index.lock"); }).find(|name: &&str| { return git_dir.join(name).symlink_metadata().is_ok(); });
176}
177
178pub fn head_commit(git_dir: &Path, common_dir: &Path, format: ObjectFormat) -> Option<String> {
188 let refs: Refs = Refs::new(common_dir, format.hex_length());
189 return match refs.lookup_head(git_dir)? {
190 RefValue::Direct(object) => Some(object),
191 RefValue::Symbolic(name) => refs.resolve(name.as_str()),
192 };
193}
194
195pub fn stored_stats(bytes: &[u8], hash_length: usize) -> Option<Vec<StatEntry>> {
205 let (entries, _end) = walk_entries(bytes, hash_length)?;
206 let mut found: Vec<StatEntry> = Vec::with_capacity(entries.len());
207 for entry in entries {
208 let stored: StoredStat = StoredStat {
209 ctime_seconds: be32(bytes, entry.start)?,
210 ctime_nanos: be32(bytes, entry.start + 4)?,
211 mtime_seconds: be32(bytes, entry.start + 8)?,
212 mtime_nanos: be32(bytes, entry.start + 12)?,
213 ino: be32(bytes, entry.start + 20)?,
214 uid: be32(bytes, entry.start + 28)?,
215 gid: be32(bytes, entry.start + 32)?,
216 size: be32(bytes, entry.start + 36)?,
217 };
218 found.push(StatEntry { path: entry.path, flags: entry.flags, mode: entry.mode, stored });
219 }
220 return Some(found);
221}
222
223fn index_differs_from_tree(index: &[IndexEntry], tree: &[FlatEntry]) -> Option<CleanRefusal> {
233 if index.len() != tree.len() {
234 return Some(CleanRefusal::new(CleanRefusalKind::StagedChange, "the index and the tree of HEAD hold different numbers of files"));
235 }
236 for (staged, committed) in index.iter().zip(tree) {
237 if staged.stage != 0 || staged.extended != 0 {
238 return Some(CleanRefusal::new(CleanRefusalKind::NotPlain, format!("the index entry {} is unmerged, intent-to-add or skip-worktree", String::from_utf8_lossy(&staged.path))));
239 }
240 if staged.mode == MODE_GITLINK {
241 return Some(CleanRefusal::new(CleanRefusalKind::NotPlain, format!("{} is a submodule", String::from_utf8_lossy(&staged.path))));
242 }
243 if staged.path != committed.path || staged.mode != committed.mode || staged.id != committed.id {
244 return Some(CleanRefusal::new(CleanRefusalKind::StagedChange, format!("{} is staged with a change", String::from_utf8_lossy(&staged.path))));
245 }
246 }
247 return None;
248}
249
250#[derive(Clone, Debug, Eq, PartialEq)]
261pub enum StatVerdict {
262 Agrees,
264 Unproven(String),
266 Differs(String),
268}
269
270pub fn metadata_verdict(entry: &StatEntry, metadata: &std::fs::Metadata, index_seconds: u32) -> StatVerdict {
282 return match stat_finding(entry, metadata, index_seconds) {
283 None => StatVerdict::Agrees,
284 Some(CleanRefusal { kind: CleanRefusalKind::Racy | CleanRefusalKind::StatStale, reason }) => StatVerdict::Unproven(reason),
285 Some(refusal) => StatVerdict::Differs(refusal.reason),
286 };
287}
288
289fn stat_finding(entry: &StatEntry, metadata: &std::fs::Metadata, index_seconds: u32) -> Option<CleanRefusal> {
301 let StatEntry { path, flags, mode, stored }: &StatEntry = entry;
302 let shown: String = String::from_utf8_lossy(path).into_owned();
303 if flags & ASSUME_VALID != 0 {
304 return Some(CleanRefusal::new(CleanRefusalKind::NotPlain, format!("{shown} is marked assume-unchanged")));
305 }
306 let symbolic: bool = *mode & 0o170_000 == 0o120_000;
307 if symbolic != metadata.file_type().is_symlink() || (!symbolic && !metadata.file_type().is_file()) {
308 return Some(CleanRefusal::new(CleanRefusalKind::FileChanged, format!("{shown} changed its type")));
309 }
310 if !symbolic && (*mode & 0o100 != 0) != (metadata.mode() & 0o100 != 0) {
311 return Some(CleanRefusal::new(CleanRefusalKind::FileChanged, format!("{shown} changed its executable bit")));
312 }
313 if stored.mtime_seconds >= index_seconds {
314 return Some(CleanRefusal::new(CleanRefusalKind::Racy, format!("{shown} is as new as the index, so its stat fields prove nothing")));
315 }
316 let now: StatFields = fields_of(metadata);
317 if !matches_stored(&now, stored) {
318 return Some(CleanRefusal::new(CleanRefusalKind::StatStale, format!("{shown} differs from its index entry by size, times or inode")));
319 }
320 return None;
321}
322
323pub fn metadata_differs_from_entry(entry: &StatEntry, metadata: &std::fs::Metadata, index_seconds: u32) -> Option<String> {
334 return match metadata_verdict(entry, metadata, index_seconds) {
335 StatVerdict::Agrees => None,
336 StatVerdict::Unproven(reason) | StatVerdict::Differs(reason) => Some(reason),
337 };
338}
339
340pub fn tree_differs_from_index(root: &Path, entries: &[StatEntry], index_seconds: u32) -> Option<String> {
352 return match open_root(root) {
353 Ok(held) => tree_differs_from_index_beneath(&held, entries, index_seconds),
354 Err(error) => Some(format!("{} cannot be opened without following a link ({error})", root.display())),
355 };
356}
357
358fn tree_refusal(root: &Path, entries: &[StatEntry], index_seconds: u32) -> Option<CleanRefusal> {
361 return match open_root(root) {
362 Ok(held) => tree_refusal_beneath(&held, entries, index_seconds),
363 Err(error) => Some(CleanRefusal::unreadable(format!("{} cannot be opened without following a link ({error})", root.display()))),
364 };
365}
366
367pub struct ContentTarget<'a> {
369 pub root: &'a Path,
371 pub git_dir: &'a Path,
373 pub common_dir: &'a Path,
375 pub environment: &'a ConversionEnvironment,
377 pub format: ObjectFormat,
379}
380
381fn tree_differs_from_index_by_content(target: &ContentTarget<'_>, entries: &[StatEntry], indexed: &[IndexEntry], index_seconds: u32) -> Option<String> {
395 if entries.len() != indexed.len() {
396 return Some(String::from("the index entries and their stat data are not the same files"));
397 }
398 let held: std::fs::File = match open_root(target.root) {
399 Ok(opened) => opened,
400 Err(error) => return Some(format!("{} cannot be opened without following a link ({error})", target.root.display())),
401 };
402 let mut judge: ContentJudge<'_> = ContentJudge::new(&held, target.git_dir, target.common_dir, target.environment, target.format);
403 let mut cached: Option<(Vec<std::ffi::OsString>, std::fs::File)> = None;
404 for (entry, committed) in entries.iter().zip(indexed) {
405 if entry.path != committed.path {
406 return Some(String::from("the index entries and their stat data are not the same files"));
407 }
408 let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
409 let Ok(names) = plain_components(Path::new(OsStr::from_bytes(entry.path.as_slice()))) else { return Some(format!("{shown} is not a plain path")) };
410 let Some((name, parents)) = names.split_last() else { return Some(format!("{shown} is not a plain path")) };
411 if cached.as_ref().is_none_or(|(known, _directory): &(Vec<std::ffi::OsString>, std::fs::File)| { return known.as_slice() != parents; }) {
412 let Ok(directory) = open_directory_beneath(&held, parents) else { return Some(format!("{shown} is missing from the worktree or reached through a link")) };
413 cached = Some((parents.to_vec(), directory));
414 }
415 let Some((_known, directory)) = cached.as_ref() else { return Some(format!("{shown} is missing from the worktree")) };
416 let Ok(metadata) = entry_metadata_at(directory, name.as_os_str()) else { return Some(format!("{shown} is missing from the worktree")) };
417 match metadata_verdict(entry, &metadata, index_seconds) {
418 StatVerdict::Agrees => {}
419 StatVerdict::Differs(reason) => return Some(reason),
420 StatVerdict::Unproven(_reason) => {
421 let place: Place<'_> = Place { directory, parents, name: name.as_os_str() };
422 if let Err(reason) = judge.agrees(&place, entry, committed.id.as_slice(), index_seconds) {
423 return Some(reason);
424 }
425 }
426 }
427 }
428 return None;
429}
430
431pub fn tree_differs_from_index_beneath(root: &std::fs::File, entries: &[StatEntry], index_seconds: u32) -> Option<String> {
443 return tree_refusal_beneath(root, entries, index_seconds).map(|refusal: CleanRefusal| { return refusal.reason; });
444}
445
446fn tree_refusal_beneath(root: &std::fs::File, entries: &[StatEntry], index_seconds: u32) -> Option<CleanRefusal> {
448 let changed = |reason: String| -> Option<CleanRefusal> { return Some(CleanRefusal::new(CleanRefusalKind::FileChanged, reason)); };
449 let mut cached: Option<(Vec<std::ffi::OsString>, std::fs::File)> = None;
450 for entry in entries {
451 let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
452 let Ok(names) = plain_components(Path::new(OsStr::from_bytes(entry.path.as_slice()))) else { return Some(CleanRefusal::unreadable(format!("{shown} is not a plain path"))) };
453 let Some((name, parents)) = names.split_last() else { return Some(CleanRefusal::unreadable(format!("{shown} is not a plain path"))) };
454 if cached.as_ref().is_none_or(|(held, _directory): &(Vec<std::ffi::OsString>, std::fs::File)| { return held.as_slice() != parents; }) {
455 let Ok(directory) = open_directory_beneath(root, parents) else { return changed(format!("{shown} is missing from the worktree or reached through a link")) };
456 cached = Some((parents.to_vec(), directory));
457 }
458 let Some((_held, directory)) = cached.as_ref() else { return changed(format!("{shown} is missing from the worktree")) };
459 let Ok(metadata) = entry_metadata_at(directory, name.as_os_str()) else { return changed(format!("{shown} is missing from the worktree")) };
460 if let Some(refusal) = stat_finding(entry, &metadata, index_seconds) {
461 return Some(refusal);
462 }
463 }
464 return None;
465}
466
467pub struct StatCursor<'a> {
478 root: &'a std::fs::File,
480 held: Option<(Vec<std::ffi::OsString>, std::fs::File)>,
482}
483
484impl<'a> StatCursor<'a> {
485 pub fn new(root: &'a std::fs::File) -> Self {
487 return Self { root, held: None };
488 }
489
490 fn metadata(&mut self, path: &[u8]) -> Result<std::fs::Metadata, String> {
492 let shown: String = String::from_utf8_lossy(path).into_owned();
493 let Ok(names) = plain_components(Path::new(OsStr::from_bytes(path))) else { return Err(format!("{shown} is not a plain path")) };
494 let Some((name, parents)) = names.split_last() else { return Err(format!("{shown} is not a plain path")) };
495 if self.held.as_ref().is_none_or(|(known, _directory): &(Vec<std::ffi::OsString>, std::fs::File)| { return known.as_slice() != parents; }) {
496 let Ok(directory) = open_directory_beneath(self.root, parents) else { return Err(format!("{shown} is missing from the worktree or reached through a link")) };
497 self.held = Some((parents.to_vec(), directory));
498 }
499 let Some((_known, directory)) = self.held.as_ref() else { return Err(format!("{shown} is missing from the worktree")) };
500 return entry_metadata_at(directory, name.as_os_str()).map_err(|_error| { return format!("{shown} is missing from the worktree"); });
501 }
502
503 pub fn fields(&mut self, path: &[u8]) -> Option<StatFields> {
505 return self.metadata(path).ok().map(|found: std::fs::Metadata| { return fields_of(&found); });
506 }
507}
508
509pub fn tree_fields_beneath(root: &std::fs::File, entries: &[StatEntry], index_seconds: u32) -> Result<Vec<StatFields>, CleanRefusal> {
522 let mut cursor: StatCursor<'_> = StatCursor::new(root);
523 let mut fields: Vec<StatFields> = Vec::with_capacity(entries.len());
524 for entry in entries {
525 let metadata: std::fs::Metadata = match cursor.metadata(entry.path.as_slice()) {
526 Ok(found) => found,
527 Err(reason) if reason.ends_with("is not a plain path") => return Err(CleanRefusal::unreadable(reason)),
528 Err(reason) => return Err(CleanRefusal::new(CleanRefusalKind::FileChanged, reason)),
529 };
530 if let Some(refusal) = stat_finding(entry, &metadata, index_seconds) {
531 return Err(refusal);
532 }
533 fields.push(fields_of(&metadata));
534 }
535 return Ok(fields);
536}
537
538pub fn stat_fields_beneath(root: &std::fs::File, path: &[u8]) -> Option<StatFields> {
549 return StatCursor::new(root).fields(path);
550}
551
552fn untracked_file(context: &GitContext, root: &Path) -> Result<Option<String>, CleanRefusal> {
563 let request: GitRequest = GitRequest {
564 cwd: root.to_path_buf(),
565 arguments: ["ls-files", "--others", "--exclude-standard", "--directory", "--no-empty-directory", "-z"].map(Into::into).to_vec(),
566 without_prefix: true,
567 ..GitRequest::default()
568 };
569 let output = run_git(context, &request).map_err(|error| { return CleanRefusal::unreadable(format!("Git could not list untracked files ({error})")); })?;
570 if output.code != Some(0) {
571 return Err(CleanRefusal::unreadable(format!("Git could not list untracked files: {}", String::from_utf8_lossy(output.stderr.as_slice()).trim())));
572 }
573 return Ok(output.stdout.split(|byte: &u8| { return *byte == 0; }).find(|name: &&[u8]| { return !name.is_empty(); }).map(|name: &[u8]| { return String::from_utf8_lossy(name).into_owned(); }));
574}
575
576pub fn check_clean_tracked(root: &Path, git_dir: &Path, common_dir: &Path) -> Result<CleanSource, String> {
588 return check_tracked(root, git_dir, common_dir, None, false).map_err(|refusal: CleanRefusal| { return refusal.reason; });
589}
590
591pub fn check_clean_tracked_by_content(root: &Path, git_dir: &Path, common_dir: &Path, environment: &ConversionEnvironment, owned_index_lock: bool) -> Result<CleanSource, String> {
604 return check_tracked(root, git_dir, common_dir, Some(environment), owned_index_lock).map_err(|refusal: CleanRefusal| { return refusal.reason; });
605}
606
607struct State {
609 bytes: Vec<u8>,
611 index_seconds: u32,
613 hash_length: usize,
615 format: ObjectFormat,
617 entries: Vec<IndexEntry>,
619 stats: Vec<StatEntry>,
621 commit: String,
623 tree_id: Vec<u8>,
625 tree: Vec<FlatEntry>,
627}
628
629impl State {
630 fn attribute_blobs(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
632 return self.tree
633 .iter()
634 .filter(|entry: &&FlatEntry| { return entry.path == b".gitattributes" || entry.path.ends_with(b"/.gitattributes"); })
635 .map(|entry: &FlatEntry| { return (entry.path.clone(), entry.id.clone()); })
636 .collect();
637 }
638
639 fn into_source(self, index_bytes: Vec<u8>) -> CleanSource {
641 let attribute_blobs: Vec<(Vec<u8>, Vec<u8>)> = self.attribute_blobs();
642 return CleanSource { head_commit: self.commit, head_tree: self.tree_id, tracked: self.entries.len(), index_seconds: self.index_seconds, hash_length: self.hash_length, attribute_blobs, index_bytes };
643 }
644}
645
646fn read_state(git_dir: &Path, common_dir: &Path) -> Result<State, CleanRefusal> {
657 let hash_length: usize = repository_hash_length(common_dir).ok_or_else(|| { return CleanRefusal::unreadable("the repository configuration is unreadable"); })?;
658 let format: ObjectFormat = if hash_length == 32 { ObjectFormat::Sha256 } else { ObjectFormat::Sha1 };
659 let index_path = git_dir.join("index");
661 let unreadable = |error: std::io::Error| -> CleanRefusal { return CleanRefusal::unreadable(format!("the index is unreadable ({error})")); };
662 let mut index_file: std::fs::File = std::fs::File::open(index_path.as_path()).map_err(unreadable)?;
663 let index_seconds: u32 = index_file.metadata().map_err(unreadable)?.mtime() as u32;
664 let mut bytes: Vec<u8> = Vec::new();
665 std::io::Read::read_to_end(&mut index_file, &mut bytes).map_err(unreadable)?;
666 let entries: Vec<IndexEntry> = parse_index(bytes.as_slice(), format).ok_or_else(|| { return CleanRefusal::unreadable("the index has a layout the reader does not know"); })?;
667 let stats = stored_stats(bytes.as_slice(), hash_length).ok_or_else(|| { return CleanRefusal::unreadable("the index entries have no plain layout"); })?;
668 let commit: String = head_commit(git_dir, common_dir, format).ok_or_else(|| { return CleanRefusal::unreadable("HEAD is not a plain ref to a commit"); })?;
669 let database: ObjectDatabase = ObjectDatabase::open(common_dir, None, format).ok_or_else(|| { return CleanRefusal::unreadable("the object database has a layout the reader does not know"); })?;
670 let commit_name: Vec<u8> = crate::repo_read_object::from_hex(commit.as_bytes(), format).ok_or_else(|| { return CleanRefusal::unreadable("HEAD names no object"); })?;
671 let commit_fields = parse_commit(database.read(commit_name.as_slice()).ok_or_else(|| { return CleanRefusal::unreadable("the commit of HEAD is unreadable"); })?.bytes.as_slice(), format)
672 .ok_or_else(|| { return CleanRefusal::unreadable("the commit of HEAD has a layout the reader does not know"); })?;
673 let tree: Vec<FlatEntry> = flatten_tree(&database, commit_fields.tree.as_slice(), format).ok_or_else(|| { return CleanRefusal::unreadable("the tree of HEAD is unreadable"); })?;
674 return Ok(State { bytes, index_seconds, hash_length, format, entries, stats, commit, tree_id: commit_fields.tree, tree });
675}
676
677fn check_tracked(root: &Path, git_dir: &Path, common_dir: &Path, environment: Option<&ConversionEnvironment>, owned_index_lock: bool) -> Result<CleanSource, CleanRefusal> {
679 if let Some(name) = operation_in_progress(git_dir, owned_index_lock) {
680 return Err(CleanRefusal::new(CleanRefusalKind::OperationInProgress(name), format!("{name} exists in {}", git_dir.display())));
681 }
682 let state: State = crate::add_timing::step("readIndexAndTree", || { return read_state(git_dir, common_dir); })?;
683 if let Some(refusal) = crate::add_timing::step("indexAgainstTree", || { return index_differs_from_tree(state.entries.as_slice(), state.tree.as_slice()); }) {
684 return Err(refusal);
685 }
686 let differs: Option<CleanRefusal> = match environment {
687 None => crate::add_timing::step("walkSource", || { return tree_refusal(root, state.stats.as_slice(), state.index_seconds); }),
688 Some(found) => tree_differs_from_index_by_content(&ContentTarget { root, git_dir, common_dir, environment: found, format: state.format }, state.stats.as_slice(), state.entries.as_slice(), state.index_seconds)
689 .map(|reason: String| { return CleanRefusal::new(CleanRefusalKind::FileChanged, reason); }),
690 };
691 if let Some(refusal) = differs {
692 return Err(refusal);
693 }
694 let bytes: Vec<u8> = state.bytes.clone();
695 return Ok(state.into_source(bytes));
696}
697
698fn entry_a_reset_cannot_undo(state: &State) -> Option<CleanRefusal> {
710 for staged in &state.entries {
711 let shown: String = String::from_utf8_lossy(&staged.path).into_owned();
712 if staged.stage != 0 || staged.extended != 0 {
713 return Some(CleanRefusal::new(CleanRefusalKind::NotPlain, format!("the index entry {shown} is unmerged, intent-to-add or skip-worktree")));
714 }
715 if staged.mode == MODE_GITLINK {
716 return Some(CleanRefusal::new(CleanRefusalKind::NotPlain, format!("{shown} is a submodule")));
717 }
718 }
719 if let Some(entry) = state.tree.iter().find(|entry: &&FlatEntry| { return entry.mode == MODE_GITLINK; }) {
720 return Some(CleanRefusal::new(CleanRefusalKind::NotPlain, format!("{} is a submodule in HEAD", String::from_utf8_lossy(&entry.path))));
721 }
722 return state.stats.iter().find(|entry: &&StatEntry| { return entry.flags & ASSUME_VALID != 0; }).map(|entry: &StatEntry| {
723 return CleanRefusal::new(CleanRefusalKind::NotPlain, format!("{} is marked assume-unchanged", String::from_utf8_lossy(&entry.path)));
724 });
725}
726
727pub fn check_clean(context: &GitContext, root: &Path, git_dir: &Path, common_dir: &Path) -> Result<CleanSource, CleanRefusal> {
739 let (tracked, untracked): (Result<CleanSource, CleanRefusal>, Result<Option<String>, CleanRefusal>) = std::thread::scope(|scope| {
742 let scan = scope.spawn(|| { return crate::add_timing::work("untrackedScan", || { return untracked_file(context, root); }); });
743 let tracked = check_tracked(root, git_dir, common_dir, None, false);
744 let scanned = scan.join().unwrap_or_else(|_panic| { return Err(CleanRefusal::unreadable("the scan for untracked files stopped unexpectedly")); });
745 return (tracked, scanned);
746 });
747 let clean: CleanSource = tracked?;
748 if let Some(name) = untracked? {
749 return Err(CleanRefusal::new(CleanRefusalKind::Untracked, format!("{name} is untracked and not ignored")));
750 }
751 return Ok(clean);
752}
753
754#[derive(Clone, Debug, Eq, PartialEq)]
766pub struct DirtySource {
767 pub source: CleanSource,
769 pub why: CleanRefusal,
771}
772
773pub fn dirty_source(git_dir: &Path, common_dir: &Path, why: CleanRefusal) -> Result<DirtySource, CleanRefusal> {
785 if !why.kind.restorable() {
786 return Err(why);
787 }
788 let state: State = read_state(git_dir, common_dir)?;
789 if let Some(refusal) = entry_a_reset_cannot_undo(&state) {
790 return Err(refusal);
791 }
792 let read: Vec<u8> = state.bytes.clone();
793 return Ok(DirtySource { source: state.into_source(read), why });
794}
795
796#[cfg(test)]
797#[path = "worktree_clean_tests.rs"]
798mod tests;