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;
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 head_tree(repo: &Repository) -> Result<Option<ObjectId>> {
209 let head = resolve_head(repo.git_dir())?;
210 Ok(match head.oid() {
211 Some(oid) => Some(tree_oid_of_commit(repo.odb(), oid)?),
212 None => None,
213 })
214}
215
216fn worktree_has_changes(
217 repo: &Repository,
218 index: &mut Index,
219 base_tree: Option<&ObjectId>,
220 matcher: &PathspecMatcher<'_>,
221 opts: &DiffOptions,
222) -> Result<bool> {
223 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
224 let config = repo.config()?;
225 let index_mtime = index_mtime_for_diff(index, None);
226 let rules_arc = Arc::new(std::sync::Mutex::new(WorktreeRules::from_repository(
227 repo, index,
228 )?));
229
230 let cache_valid =
231 crate::index::write_tree::cache_tree_extension_valid(index.cache_tree.as_ref());
232 let index_backed = match base_tree {
233 None => true,
235 Some(tree) => {
236 index
237 .cache_tree_root
238 .as_ref()
239 .is_some_and(|root| root == tree)
240 && cache_valid
241 }
242 };
243
244 if index_backed {
245 WORKTREE_HAS_CHANGES_ENTRIES_SCANNED.store(0, Ordering::SeqCst);
246 let content_compare = opts_need_content_compare(opts);
247 let (entries, _) = diff_index_to_worktree_with_options(
248 repo.odb(),
249 index,
250 work_tree,
251 DiffIndexToWorktreeOptions {
252 repository_git_dir: Some(repo.git_dir().to_path_buf()),
253 index_mtime,
254 config: Some(Arc::clone(&config)),
255 thread_policy: Some(repo.thread_policy()),
256 refresh_index_stat_in_pass: false,
257 for_status: !content_compare,
258 worktree_rules: Some(Arc::clone(&rules_arc)),
259 ..DiffIndexToWorktreeOptions::default()
260 },
261 )?;
262 for entry in &entries {
263 WORKTREE_HAS_CHANGES_ENTRIES_SCANNED.fetch_add(1, Ordering::SeqCst);
264 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
265 continue;
266 }
267 if worktree_index_entry_has_changes(
268 entry,
269 trust_filemode(config.as_ref()),
270 repo.odb(),
271 work_tree,
272 opts,
273 )? {
274 return Ok(true);
275 }
276 }
277 return Ok(false);
278 }
279
280 let worktree_rules = WorktreeRules::from_repository(repo, index)?;
281 let raw = diff_tree_to_worktree_with_git_dir_and_rules(
282 repo.odb(),
283 base_tree,
284 work_tree,
285 repo.git_dir(),
286 index,
287 Some(config.as_ref()),
288 Some(&worktree_rules),
289 )?;
290 let entries = apply_rename_detection_to_diff_entries(
291 repo.odb(),
292 raw,
293 Some(work_tree),
294 rename_threshold(opts),
295 config.as_ref(),
296 &repo.thread_policy(),
297 )?;
298 for entry in entries {
299 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
300 continue;
301 }
302 if tree_entry_has_changes(repo.odb(), &entry, trust_filemode(config.as_ref()), opts)? {
303 return Ok(true);
304 }
305 }
306 Ok(false)
307}
308
309fn staged_has_changes(
310 repo: &Repository,
311 matcher: &PathspecMatcher<'_>,
312 opts: &DiffOptions,
313) -> Result<bool> {
314 let head_tree = head_tree(repo)?;
315 let index = repo.index_handle().load()?;
316 let config = repo.config()?;
317 let mut entries = diff_index_to_tree_with_renames(
318 repo.odb(),
319 &index,
320 head_tree.as_ref(),
321 false,
322 rename_threshold(opts),
323 config.as_ref(),
324 &repo.thread_policy(),
325 )?;
326 for entry in entries.drain(..) {
327 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
328 continue;
329 }
330 if tree_entry_has_changes(repo.odb(), &entry, trust_filemode(config.as_ref()), opts)? {
331 return Ok(true);
332 }
333 }
334 Ok(false)
335}
336
337fn unstaged_has_changes(
338 repo: &Repository,
339 matcher: &PathspecMatcher<'_>,
340 opts: &DiffOptions,
341) -> Result<bool> {
342 let mut index = repo.index_handle().load()?;
343 worktree_has_changes(repo, &mut index, None, matcher, opts)
344}
345
346fn commit_trees_has_changes(
347 repo: &Repository,
348 base: &ObjectId,
349 tip: &ObjectId,
350 matcher: &PathspecMatcher<'_>,
351 opts: &DiffOptions,
352) -> Result<bool> {
353 let config = repo.config()?;
354 let base_tree = tree_oid_of_commit(repo.odb(), base)?;
355 let tip_tree = tree_oid_of_commit(repo.odb(), tip)?;
356 let mut entries = diff_trees_with_renames_and_policy(
357 repo.odb(),
358 Some(&base_tree),
359 Some(&tip_tree),
360 "",
361 rename_threshold(opts),
362 config.as_ref(),
363 &repo.thread_policy(),
364 )?;
365 for entry in entries.drain(..) {
366 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
367 continue;
368 }
369 if tree_entry_has_changes(repo.odb(), &entry, trust_filemode(config.as_ref()), opts)? {
370 return Ok(true);
371 }
372 }
373 Ok(false)
374}
375
376impl<'repo> super::handle::Diff<'repo> {
377 pub fn has_changes(
387 &self,
388 target: &DiffTarget,
389 pathspecs: &[String],
390 opts: DiffOptions,
391 ) -> Result<bool> {
392 HAS_CHANGES_CALL_COUNT.fetch_add(1, Ordering::SeqCst);
393 let repo = self.repository();
394 let matcher = repo.pathspec_matcher(pathspecs)?;
395 match target {
396 DiffTarget::WorktreeVsHead => {
397 let mut index = repo.index_handle().load()?;
398 let head_tree = head_tree(repo)?;
399 worktree_has_changes(repo, &mut index, head_tree.as_ref(), &matcher, &opts)
400 }
401 DiffTarget::IndexVsHead => staged_has_changes(repo, &matcher, &opts),
402 DiffTarget::WorktreeVsIndex => unstaged_has_changes(repo, &matcher, &opts),
403 DiffTarget::WorktreeVsTree(tree) => {
404 let mut index = repo.index_handle().load()?;
405 worktree_has_changes(repo, &mut index, Some(tree), &matcher, &opts)
406 }
407 DiffTarget::Commit(commit) => {
408 let obj = repo.odb().read(commit)?;
409 let parsed = parse_commit(&obj.data)?;
410 let new_tree = parsed.tree;
411 let old_tree = parsed
412 .parents
413 .first()
414 .map(|parent| tree_oid_of_commit(repo.odb(), parent))
415 .transpose()?;
416 let config = repo.config()?;
417 let mut entries = diff_trees_with_renames_and_policy(
418 repo.odb(),
419 old_tree.as_ref(),
420 Some(&new_tree),
421 "",
422 rename_threshold(&opts),
423 config.as_ref(),
424 &repo.thread_policy(),
425 )?;
426 for entry in entries.drain(..) {
427 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
428 continue;
429 }
430 if tree_entry_has_changes(
431 repo.odb(),
432 &entry,
433 trust_filemode(config.as_ref()),
434 &opts,
435 )? {
436 return Ok(true);
437 }
438 }
439 Ok(false)
440 }
441 DiffTarget::Trees { old, new } => match old {
442 Some(base) => commit_trees_has_changes(repo, base, new, &matcher, &opts),
443 None => {
444 let config = repo.config()?;
445 let tip_tree = tree_oid_of_commit(repo.odb(), new)?;
446 let mut entries = diff_trees_with_renames_and_policy(
447 repo.odb(),
448 None,
449 Some(&tip_tree),
450 "",
451 rename_threshold(&opts),
452 config.as_ref(),
453 &repo.thread_policy(),
454 )?;
455 for entry in entries.drain(..) {
456 if !matcher.matches_change(entry.path(), entry.old_path.as_deref()) {
457 continue;
458 }
459 if tree_entry_has_changes(
460 repo.odb(),
461 &entry,
462 trust_filemode(config.as_ref()),
463 &opts,
464 )? {
465 return Ok(true);
466 }
467 }
468 Ok(false)
469 }
470 },
471 }
472 }
473}