1use std::path::Path;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::Arc;
6
7use crate::config::ConfigSet;
8use crate::diff::DiffTarget;
9use crate::diff::{
10 apply_rename_detection_to_diff_entries, diff_index_to_tree_with_renames,
11 diff_index_to_worktree_with_options, diff_tree_to_worktree_with_git_dir_and_rules,
12 diff_trees_with_renames_and_policy, index_mtime_for_diff, merge_file, tree_oid_of_commit,
13 DiffEntry, DiffIndexToWorktreeOptions, DiffOptions, DiffStatus,
14};
15use crate::error::{Error, Result};
16use crate::index::Index;
17use crate::objects::odb::Odb;
18use crate::objects::{parse_commit, ObjectId};
19use crate::pathspec::PathspecMatcher;
20use crate::repo::Repository;
21use crate::state::resolve_head_for_repository;
22use crate::worktree::worktree_rules::WorktreeRules;
23
24#[doc(hidden)]
26pub static WORKTREE_HAS_CHANGES_ENTRIES_SCANNED: AtomicUsize = AtomicUsize::new(0);
27
28#[doc(hidden)]
30pub static HAS_CHANGES_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
31
32fn opts_need_content_compare(opts: &DiffOptions) -> bool {
33 opts.whitespace.is_active()
34}
35
36fn trust_filemode(config: &ConfigSet) -> bool {
37 config
38 .get_bool("core.filemode")
39 .and_then(|r| r.ok())
40 .unwrap_or(true)
41}
42
43fn rename_threshold(opts: &DiffOptions) -> u32 {
44 opts.renames.threshold_percent().unwrap_or(101)
45}
46
47#[must_use]
49pub fn text_sides_differ(old: &str, new: &str, opts: &DiffOptions) -> bool {
50 if old == new {
51 return false;
52 }
53 let ws = opts.whitespace;
54 let (a, b) = preprocess_for_whitespace(old, new, ws);
55 if ws.ignore_blank_lines {
56 let a: Vec<_> = a.lines().filter(|l| !l.trim().is_empty()).collect();
57 let b: Vec<_> = b.lines().filter(|l| !l.trim().is_empty()).collect();
58 return a != b;
59 }
60 a != b
61}
62
63fn preprocess_for_whitespace(
64 old: &str,
65 new: &str,
66 ws: crate::diff::WhitespaceMode,
67) -> (String, String) {
68 if ws.ignore_all_space {
69 return (strip_all_whitespace(old), strip_all_whitespace(new));
70 }
71 if ws.ignore_space_change {
72 return (
73 crate::diff::normalize_ignore_space_change(old),
74 crate::diff::normalize_ignore_space_change(new),
75 );
76 }
77 if ws.ignore_space_at_eol {
78 return (trim_trailing_ws_lines(old), trim_trailing_ws_lines(new));
79 }
80 (old.to_owned(), new.to_owned())
81}
82
83fn strip_all_whitespace(s: &str) -> String {
84 s.lines()
85 .map(|line| {
86 line.chars()
87 .filter(|c| !c.is_whitespace())
88 .collect::<String>()
89 })
90 .collect::<Vec<_>>()
91 .join("\n")
92}
93
94fn trim_trailing_ws_lines(s: &str) -> String {
95 s.lines()
96 .map(|line| line.trim_end_matches(|c: char| c.is_whitespace()))
97 .collect::<Vec<_>>()
98 .join("\n")
99}
100
101fn worktree_file_index_blob_differ(
102 odb: &Odb,
103 work_tree: &Path,
104 rel_path: &str,
105 index_oid: &ObjectId,
106 opts: &DiffOptions,
107) -> Result<bool> {
108 let file_path = work_tree.join(rel_path);
109 let wt_bytes = std::fs::read(&file_path).map_err(Error::Io)?;
110 let index_raw = odb.read(index_oid)?.data;
111 if merge_file::is_binary(&wt_bytes) || merge_file::is_binary(&index_raw) {
112 return Ok(wt_bytes != index_raw);
113 }
114 let wt_s = String::from_utf8_lossy(&wt_bytes);
115 let idx_s = String::from_utf8_lossy(&index_raw);
116 Ok(text_sides_differ(&idx_s, &wt_s, opts))
117}
118
119fn worktree_index_entry_has_changes(
120 entry: &DiffEntry,
121 trust_fm: bool,
122 odb: &Odb,
123 work_tree: &Path,
124 opts: &DiffOptions,
125) -> Result<bool> {
126 match entry.status {
127 DiffStatus::Added
128 | DiffStatus::Deleted
129 | DiffStatus::TypeChanged
130 | DiffStatus::Unmerged => {
131 return Ok(true);
132 }
133 DiffStatus::Renamed | DiffStatus::Copied => return Ok(true),
134 DiffStatus::Modified => {}
135 }
136 if entry.old_mode != entry.new_mode && trust_fm {
137 return Ok(true);
138 }
139 let rel_path = entry.path();
140 let content_aware = opts_need_content_compare(opts);
141 if entry.new_oid.is_zero() && !entry.old_oid.is_zero() {
142 if content_aware {
143 return worktree_file_index_blob_differ(odb, work_tree, rel_path, &entry.old_oid, opts);
144 }
145 return Ok(true);
146 }
147 if entry.old_oid != entry.new_oid {
148 return blob_bytes_differ(odb, &entry.old_oid, &entry.new_oid, opts);
149 }
150 if content_aware {
151 return worktree_file_index_blob_differ(odb, work_tree, rel_path, &entry.old_oid, opts);
152 }
153 Ok(true)
154}
155
156fn blob_bytes_differ(
157 odb: &Odb,
158 old_oid: &ObjectId,
159 new_oid: &ObjectId,
160 opts: &DiffOptions,
161) -> Result<bool> {
162 if old_oid == new_oid {
163 return Ok(false);
164 }
165 let old_raw = odb.read(old_oid)?.data;
166 let new_raw = odb.read(new_oid)?.data;
167 if merge_file::is_binary(&old_raw) || merge_file::is_binary(&new_raw) {
168 return Ok(old_raw != new_raw);
169 }
170 let old_s = String::from_utf8_lossy(&old_raw);
171 let new_s = String::from_utf8_lossy(&new_raw);
172 Ok(text_sides_differ(&old_s, &new_s, opts))
173}
174
175pub(crate) fn tree_entry_has_changes(
177 odb: &Odb,
178 entry: &DiffEntry,
179 trust_fm: bool,
180 opts: &DiffOptions,
181) -> Result<bool> {
182 match entry.status {
183 DiffStatus::Added
184 | DiffStatus::Deleted
185 | DiffStatus::TypeChanged
186 | DiffStatus::Unmerged => {
187 return Ok(true);
188 }
189 DiffStatus::Renamed | DiffStatus::Copied => return Ok(true),
190 DiffStatus::Modified => {}
191 }
192 if entry.old_mode != entry.new_mode && trust_fm {
193 return Ok(true);
194 }
195 if entry.new_oid.is_zero() && !entry.old_oid.is_zero() {
197 return Ok(true);
198 }
199 if entry.old_oid.is_zero() && !entry.new_oid.is_zero() {
200 return Ok(true);
201 }
202 if entry.old_oid != entry.new_oid {
203 return blob_bytes_differ(odb, &entry.old_oid, &entry.new_oid, opts);
204 }
205 Ok(false)
206}
207
208fn worktree_has_changes(
209 repo: &Repository,
210 index: &mut Index,
211 base_tree: Option<&ObjectId>,
212 matcher: &PathspecMatcher<'_>,
213 opts: &DiffOptions,
214) -> Result<bool> {
215 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
216 let config = repo.config()?;
217 let index_mtime = index_mtime_for_diff(index, None);
218 let rules_arc = Arc::new(std::sync::Mutex::new(WorktreeRules::from_repository(
219 repo, index,
220 )?));
221
222 let cache_valid =
223 crate::index::write_tree::cache_tree_extension_valid(index.cache_tree.as_ref());
224 let index_backed = match base_tree {
225 None => true,
227 Some(tree) => {
228 index
229 .cache_tree_root
230 .as_ref()
231 .is_some_and(|root| root == tree)
232 && cache_valid
233 }
234 };
235
236 if index_backed {
237 WORKTREE_HAS_CHANGES_ENTRIES_SCANNED.store(0, Ordering::SeqCst);
238 let content_compare = opts_need_content_compare(opts);
239 let (entries, _) = diff_index_to_worktree_with_options(
240 repo.odb(),
241 index,
242 work_tree,
243 DiffIndexToWorktreeOptions {
244 repository_git_dir: Some(repo.git_dir().to_path_buf()),
245 index_mtime,
246 config: Some(Arc::clone(&config)),
247 thread_policy: Some(repo.thread_policy()),
248 refresh_index_stat_in_pass: false,
249 for_status: !content_compare,
250 worktree_rules: Some(Arc::clone(&rules_arc)),
251 ..DiffIndexToWorktreeOptions::default()
252 },
253 )?;
254 for entry in &entries {
255 WORKTREE_HAS_CHANGES_ENTRIES_SCANNED.fetch_add(1, Ordering::SeqCst);
256 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
257 continue;
258 }
259 if worktree_index_entry_has_changes(
260 entry,
261 trust_filemode(config.as_ref()),
262 repo.odb(),
263 work_tree,
264 opts,
265 )? {
266 return Ok(true);
267 }
268 }
269 return Ok(false);
270 }
271
272 let worktree_rules = WorktreeRules::from_repository(repo, index)?;
273 let raw = diff_tree_to_worktree_with_git_dir_and_rules(
274 repo.odb(),
275 base_tree,
276 work_tree,
277 repo.git_dir(),
278 index,
279 Some(config.as_ref()),
280 Some(&worktree_rules),
281 )?;
282 let entries = apply_rename_detection_to_diff_entries(
283 repo.odb(),
284 raw,
285 Some(work_tree),
286 rename_threshold(opts),
287 config.as_ref(),
288 &repo.thread_policy(),
289 )?;
290 for entry in entries {
291 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
292 continue;
293 }
294 if tree_entry_has_changes(repo.odb(), &entry, trust_filemode(config.as_ref()), opts)? {
295 return Ok(true);
296 }
297 }
298 Ok(false)
299}
300
301fn staged_has_changes(
302 repo: &Repository,
303 matcher: &PathspecMatcher<'_>,
304 opts: &DiffOptions,
305) -> Result<bool> {
306 let head_tree = resolve_head_for_repository(repo)?.tree(repo.odb())?;
307 let index = repo.index_handle().load()?;
308 let config = repo.config()?;
309 let mut entries = diff_index_to_tree_with_renames(
310 repo.odb(),
311 &index,
312 head_tree.as_ref(),
313 false,
314 rename_threshold(opts),
315 config.as_ref(),
316 &repo.thread_policy(),
317 )?;
318 for entry in entries.drain(..) {
319 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
320 continue;
321 }
322 if tree_entry_has_changes(repo.odb(), &entry, trust_filemode(config.as_ref()), opts)? {
323 return Ok(true);
324 }
325 }
326 Ok(false)
327}
328
329fn unstaged_has_changes(
330 repo: &Repository,
331 matcher: &PathspecMatcher<'_>,
332 opts: &DiffOptions,
333) -> Result<bool> {
334 let mut index = repo.index_handle().load()?;
335 worktree_has_changes(repo, &mut index, None, matcher, opts)
336}
337
338fn commit_trees_has_changes(
339 repo: &Repository,
340 base: &ObjectId,
341 tip: &ObjectId,
342 matcher: &PathspecMatcher<'_>,
343 opts: &DiffOptions,
344) -> Result<bool> {
345 let config = repo.config()?;
346 let base_tree = tree_oid_of_commit(repo.odb(), base)?;
347 let tip_tree = tree_oid_of_commit(repo.odb(), tip)?;
348 let mut entries = diff_trees_with_renames_and_policy(
349 repo.odb(),
350 Some(&base_tree),
351 Some(&tip_tree),
352 "",
353 rename_threshold(opts),
354 config.as_ref(),
355 &repo.thread_policy(),
356 )?;
357 for entry in entries.drain(..) {
358 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
359 continue;
360 }
361 if tree_entry_has_changes(repo.odb(), &entry, trust_filemode(config.as_ref()), opts)? {
362 return Ok(true);
363 }
364 }
365 Ok(false)
366}
367
368impl<'repo> super::handle::Diff<'repo> {
369 pub fn has_changes(
379 &self,
380 target: &DiffTarget,
381 pathspecs: &[String],
382 opts: DiffOptions,
383 ) -> Result<bool> {
384 HAS_CHANGES_CALL_COUNT.fetch_add(1, Ordering::SeqCst);
385 let repo = self.repository();
386 let matcher = repo.pathspec_matcher(pathspecs)?;
387 match target {
388 DiffTarget::WorktreeVsHead => {
389 let mut index = repo.index_handle().load()?;
390 let head_tree = resolve_head_for_repository(repo)?.tree(repo.odb())?;
391 worktree_has_changes(repo, &mut index, head_tree.as_ref(), &matcher, &opts)
392 }
393 DiffTarget::IndexVsHead => staged_has_changes(repo, &matcher, &opts),
394 DiffTarget::WorktreeVsIndex => unstaged_has_changes(repo, &matcher, &opts),
395 DiffTarget::WorktreeVsTree(tree) => {
396 let mut index = repo.index_handle().load()?;
397 worktree_has_changes(repo, &mut index, Some(tree), &matcher, &opts)
398 }
399 DiffTarget::Commit(commit) => {
400 let obj = repo.odb().read(commit)?;
401 let parsed = parse_commit(&obj.data)?;
402 let new_tree = parsed.tree;
403 let old_tree = parsed
404 .parents
405 .first()
406 .map(|parent| tree_oid_of_commit(repo.odb(), parent))
407 .transpose()?;
408 let config = repo.config()?;
409 let mut entries = diff_trees_with_renames_and_policy(
410 repo.odb(),
411 old_tree.as_ref(),
412 Some(&new_tree),
413 "",
414 rename_threshold(&opts),
415 config.as_ref(),
416 &repo.thread_policy(),
417 )?;
418 for entry in entries.drain(..) {
419 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
420 continue;
421 }
422 if tree_entry_has_changes(
423 repo.odb(),
424 &entry,
425 trust_filemode(config.as_ref()),
426 &opts,
427 )? {
428 return Ok(true);
429 }
430 }
431 Ok(false)
432 }
433 DiffTarget::Trees { old, new } => match old {
434 Some(base) => commit_trees_has_changes(repo, base, new, &matcher, &opts),
435 None => {
436 let config = repo.config()?;
437 let tip_tree = super::core::tree_oid_for_diff_side(repo.odb(), new)?;
438 let mut entries = diff_trees_with_renames_and_policy(
439 repo.odb(),
440 None,
441 Some(&tip_tree),
442 "",
443 rename_threshold(&opts),
444 config.as_ref(),
445 &repo.thread_policy(),
446 )?;
447 for entry in entries.drain(..) {
448 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
449 continue;
450 }
451 if tree_entry_has_changes(
452 repo.odb(),
453 &entry,
454 trust_filemode(config.as_ref()),
455 &opts,
456 )? {
457 return Ok(true);
458 }
459 }
460 Ok(false)
461 }
462 },
463 }
464 }
465}