1use std::{collections::BTreeSet, fs};
4
5use crate::{
6 Repository, Result, StatusKind, gitignore::IgnoreStack, worktree_walk::count_untracked,
7};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum WorktreeKind {
12 Bare,
14 Primary,
16 Linked,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum WorktreeSafetyLevel {
23 Clean,
25 IgnoredOnly,
27 HasUntracked,
29 DirtyTracked,
31 Unknown,
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum WorktreeEvidence {
38 TrackedDirty {
40 path: Vec<u8>,
42 index: StatusKind,
44 worktree: StatusKind,
46 },
47 Untracked {
49 path: Vec<u8>,
51 },
52 Ignored {
54 path: Vec<u8>,
56 },
57 Submodule {
59 path: Vec<u8>,
61 },
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct WorktreeSafety {
67 pub tracked_dirty: bool,
69 pub staged_dirty: bool,
71 pub untracked_count: u64,
73 pub ignored_count: u64,
75 pub submodule_unknown: bool,
77 pub kind: WorktreeKind,
79 pub level: WorktreeSafetyLevel,
81 pub evidence: Vec<WorktreeEvidence>,
83}
84
85impl WorktreeSafety {
86 fn from_parts(
87 kind: WorktreeKind,
88 tracked_dirty: bool,
89 staged_dirty: bool,
90 untracked_count: u64,
91 ignored_count: u64,
92 submodule_unknown: bool,
93 evidence: Vec<WorktreeEvidence>,
94 ) -> Self {
95 let level = if kind == WorktreeKind::Bare {
96 WorktreeSafetyLevel::Unknown
97 } else if tracked_dirty {
98 WorktreeSafetyLevel::DirtyTracked
99 } else if untracked_count > 0 {
100 WorktreeSafetyLevel::HasUntracked
101 } else if ignored_count > 0 {
102 WorktreeSafetyLevel::IgnoredOnly
103 } else {
104 WorktreeSafetyLevel::Clean
105 };
106 Self {
107 tracked_dirty,
108 staged_dirty,
109 untracked_count,
110 ignored_count,
111 submodule_unknown,
112 kind,
113 level,
114 evidence,
115 }
116 }
117}
118
119impl Repository {
120 pub fn worktree_safety(&self) -> Result<WorktreeSafety> {
127 let kind = worktree_kind(self);
128 let Some(root) = self.work_dir() else {
129 return Ok(WorktreeSafety::from_parts(
130 kind,
131 false,
132 false,
133 0,
134 0,
135 false,
136 Vec::new(),
137 ));
138 };
139 let status = self.status()?;
140 let mut staged_dirty = false;
141 let mut tracked_dirty = false;
142 let mut evidence = Vec::new();
143 for entry in &status {
144 let staged = entry.index != StatusKind::Unmodified;
145 let dirty = staged || entry.worktree != StatusKind::Unmodified;
146 staged_dirty |= staged;
147 tracked_dirty |= dirty;
148 if dirty && evidence.len() < 32 {
149 evidence.push(WorktreeEvidence::TrackedDirty {
150 path: entry.path.clone(),
151 index: entry.index,
152 worktree: entry.worktree,
153 });
154 }
155 }
156 let index = self.index_shared()?;
157 let mut submodule_unknown = false;
158 let mut indexed = BTreeSet::new();
159 for entry in index.entries() {
160 indexed.insert(entry.path.clone());
161 if entry.mode & 0o170_000 == 0o160_000 {
162 submodule_unknown = true;
163 evidence.push(WorktreeEvidence::Submodule {
164 path: entry.path.clone(),
165 });
166 }
167 }
168 let mut ignore = IgnoreStack::default();
169 if let Ok(text) = fs::read_to_string(self.git_dir().join("info").join("exclude")) {
170 ignore.push_file("", &text);
171 }
172 let counts = count_untracked(root, &indexed, &ignore, self.limits().max_index_entries)?;
173 evidence.extend(
174 counts
175 .untracked_samples
176 .into_iter()
177 .map(|path| WorktreeEvidence::Untracked { path }),
178 );
179 evidence.extend(
180 counts
181 .ignored_samples
182 .into_iter()
183 .map(|path| WorktreeEvidence::Ignored { path }),
184 );
185 Ok(WorktreeSafety::from_parts(
186 kind,
187 tracked_dirty,
188 staged_dirty,
189 counts.untracked,
190 counts.ignored,
191 submodule_unknown,
192 evidence,
193 ))
194 }
195}
196
197fn worktree_kind(repository: &Repository) -> WorktreeKind {
198 let Some(work_dir) = repository.work_dir() else {
199 return WorktreeKind::Bare;
200 };
201 if work_dir.join(".git").is_file() {
202 WorktreeKind::Linked
203 } else {
204 WorktreeKind::Primary
205 }
206}
207
208impl WorktreeSafety {
209 #[must_use]
211 pub fn unknown() -> Self {
212 Self::from_parts(WorktreeKind::Bare, false, false, 0, 0, false, Vec::new())
213 }
214}