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