1use std::collections::BTreeSet;
10use std::path::Path;
11use std::sync::{Arc, Mutex};
12
13use crate::diff::structured::{structured_file_diff, SideBytes, StructuredFileDiff};
14use crate::diff::targets::{commit_tree, diff_index_to_tree_for_paths};
15use crate::diff::tree_content::{gitlink_line, load_new_side, load_old_side};
16use crate::diff::{
17 apply_rename_detection_to_diff_entries, diff_index_to_tree_with_renames,
18 diff_index_to_worktree_with_renames, diff_tree_to_worktree_with_git_dir_and_rules,
19 index_mtime_for_diff, line_change_counts, load_worktree_side_for_diff, read_submodule_head_oid,
20 sort_entries_by_path, submodule_porcelain_flags, DiffEntry, DiffIndexToWorktreeOptions,
21 DiffOptions, DiffStatus, LineStat, RenameDetection, RENAME_DETECTION_DISABLED,
22};
23use crate::error::{Error, Result};
24use crate::index::write_tree::cache_tree_extension_valid;
25use crate::index::Index;
26use crate::objects::odb::Odb;
27use crate::objects::{FileMode, ObjectId};
28use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
29use crate::repo::Repository;
30use crate::state::resolve_head;
31use crate::worktree::fsmonitor::fsmonitor_enabled;
32use crate::worktree::worktree_rules::WorktreeRules;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct FileChange {
37 pub path: String,
39 pub old_path: Option<String>,
41 pub status: DiffStatus,
43 pub old_mode: FileMode,
45 pub new_mode: FileMode,
47 pub old_oid: ObjectId,
49 pub new_oid: ObjectId,
51 pub old_bytes: SideBytes,
53 pub new_bytes: SideBytes,
55 pub binary: bool,
57 pub content_changed: bool,
59 pub similarity: Option<u32>,
61}
62
63impl FileChange {
64 fn from_entry(
65 entry: DiffEntry,
66 old_bytes: SideBytes,
67 new_bytes: SideBytes,
68 binary: bool,
69 ) -> Self {
70 let path = entry
71 .new_path
72 .clone()
73 .or_else(|| entry.old_path.clone())
74 .unwrap_or_default();
75 let similarity = entry.rename_similarity();
76 let old_path = entry.old_path.filter(|old| *old != path);
78 Self {
79 path,
80 old_path,
81 status: entry.status,
82 old_mode: entry.old_mode,
83 new_mode: entry.new_mode,
84 old_oid: entry.old_oid,
85 new_oid: entry.new_oid,
86 old_bytes,
87 new_bytes,
88 binary,
89 content_changed: entry.old_oid != entry.new_oid,
90 similarity,
91 }
92 }
93
94 #[must_use]
102 pub fn structured(&self, options: DiffOptions) -> StructuredFileDiff {
103 if self.binary {
104 return StructuredFileDiff {
105 old_mode: self.old_mode.octal_if_present(),
106 new_mode: self.new_mode.octal_if_present(),
107 old_encoding_lossy: self.old_bytes.encoding_lossy,
108 new_encoding_lossy: self.new_bytes.encoding_lossy,
109 hunks: Vec::new(),
110 };
111 }
112 structured_file_diff(
113 self.old_mode,
114 self.new_mode,
115 &self.old_bytes,
116 &self.new_bytes,
117 options,
118 )
119 }
120
121 #[must_use]
127 pub fn line_stat(&self, options: &DiffOptions) -> Option<LineStat> {
128 if self.binary {
129 return None;
130 }
131 Some(line_change_counts(
132 &self.old_bytes.text(),
133 &self.new_bytes.text(),
134 options,
135 ))
136 }
137}
138
139pub fn load_object_changes(repo: &Repository, entries: &[DiffEntry]) -> Result<Vec<FileChange>> {
154 let config = repo.config()?;
155 let par_opts = repo
156 .thread_policy()
157 .resolve(ParallelOp::DiffContent, config.as_ref());
158 let odb = repo.odb();
159 let est_bytes = entries.len().saturating_mul(8192);
160 try_par_map(entries, par_opts, est_bytes, |entry| {
161 load_change(odb, entry.clone(), None)
162 })
163 .map_err(|ParallelHashError::Task(err)| err)
164}
165
166pub(crate) struct WorktreeSide<'a> {
168 pub(crate) work_tree: &'a Path,
169 pub(crate) index: &'a Index,
170 pub(crate) rules: &'a Mutex<WorktreeRules>,
171}
172
173pub(crate) fn load_change(
176 odb: &Odb,
177 entry: DiffEntry,
178 worktree: Option<&WorktreeSide<'_>>,
179) -> Result<FileChange> {
180 let (old_bytes, old_binary) = load_old_side(odb, &entry, None)?;
181 let (new_bytes, new_binary) = match worktree {
182 Some(side) => load_worktree_new_side(odb, &entry, side)?,
183 None => load_new_side(odb, &entry, None)?,
184 };
185 Ok(FileChange::from_entry(
186 entry,
187 old_bytes,
188 new_bytes,
189 old_binary || new_binary,
190 ))
191}
192
193pub(crate) fn load_changes(
195 odb: &Odb,
196 entries: Vec<DiffEntry>,
197 worktree: Option<&WorktreeSide<'_>>,
198) -> Result<Vec<FileChange>> {
199 entries
200 .into_iter()
201 .map(|entry| load_change(odb, entry, worktree))
202 .collect()
203}
204
205fn load_worktree_new_side(
206 odb: &Odb,
207 entry: &DiffEntry,
208 side: &WorktreeSide<'_>,
209) -> Result<(SideBytes, bool)> {
210 if entry.new_mode == FileMode::GITLINK {
211 let line = worktree_gitlink_line(entry, side.work_tree);
212 return Ok((SideBytes::from_bytes(line.into_bytes()), false));
213 }
214 if entry.new_mode == FileMode::ABSENT {
215 return Ok((SideBytes::from_bytes(Vec::new()), false));
216 }
217 let Some(path) = entry.new_path.as_deref().or(entry.old_path.as_deref()) else {
218 return load_new_side(odb, entry, None);
219 };
220 let index_entry = side.index.get(&crate::repo_relpath::storage_bytes(path), 0);
223 let rules = side.rules.lock().unwrap_or_else(|e| e.into_inner());
224 load_worktree_side_for_diff(
225 odb,
226 side.work_tree,
227 path,
228 entry.new_mode.bits(),
229 rules.config(),
230 index_entry,
231 Some(&rules),
232 false,
233 )
234}
235
236fn worktree_gitlink_line(entry: &DiffEntry, work_tree: &Path) -> String {
238 let path = entry.new_path.as_deref().or(entry.old_path.as_deref());
239 let head = if entry.new_oid != ObjectId::zero() {
240 Some(entry.new_oid)
241 } else {
242 path.and_then(|p| read_submodule_head_oid(&work_tree.join(p)))
243 };
244 let Some(head) = head.filter(|oid| *oid != ObjectId::zero()) else {
245 return String::new();
246 };
247 let dirty = entry.old_mode == FileMode::GITLINK
248 && path.is_some_and(|p| submodule_porcelain_flags(work_tree, p, entry.old_oid).modified);
249 if dirty {
250 format!("Subproject commit {}-dirty\n", head.to_hex())
251 } else {
252 gitlink_line(&head)
253 }
254}
255
256fn rename_threshold(renames: RenameDetection) -> u32 {
257 renames
258 .threshold_percent()
259 .unwrap_or(RENAME_DETECTION_DISABLED)
260}
261
262pub(crate) fn head_tree(repo: &Repository) -> Result<Option<ObjectId>> {
264 resolve_head(repo.git_dir())?
265 .oid()
266 .map(|oid| commit_tree(repo, oid))
267 .transpose()
268}
269
270pub(crate) fn staged_entries_in(
274 repo: &Repository,
275 index: &Index,
276 rename_threshold: u32,
277 restrict_to_paths: Option<&BTreeSet<String>>,
278) -> Result<Vec<DiffEntry>> {
279 let head_tree = head_tree(repo)?;
280 let mut entries = match restrict_to_paths.filter(|paths| !paths.is_empty()) {
281 Some(paths) => diff_index_to_tree_for_paths(repo, index, head_tree.as_ref(), paths)?,
282 None => {
283 let config = repo.config()?;
284 diff_index_to_tree_with_renames(
285 repo.odb(),
286 index,
287 head_tree.as_ref(),
288 false,
289 rename_threshold,
290 config.as_ref(),
291 &repo.thread_policy(),
292 )?
293 }
294 };
295 sort_entries_by_path(&mut entries);
296 Ok(entries)
297}
298
299pub(crate) fn unstaged_entries_in(
303 repo: &Repository,
304 index: &mut Index,
305 work_tree: &Path,
306 rules: &Arc<Mutex<WorktreeRules>>,
307 rename_threshold: u32,
308 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
309) -> Result<Vec<DiffEntry>> {
310 let config = rules.lock().unwrap_or_else(|e| e.into_inner()).config_arc();
311 let refresh_fsmonitor = restrict_to_paths.is_none() && fsmonitor_enabled(config.as_ref());
312 let options = DiffIndexToWorktreeOptions {
313 repository_git_dir: Some(repo.git_dir().to_path_buf()),
314 index_mtime: index_mtime_for_diff(index, None),
315 config: Some(config),
316 thread_policy: Some(repo.thread_policy()),
317 refresh_index_stat_in_pass: false,
318 refresh_fsmonitor,
319 worktree_rules: Some(Arc::clone(rules)),
320 command_runner: Some(repo.command_runner()),
321 restrict_to_paths,
322 ..DiffIndexToWorktreeOptions::default()
323 };
324 let (mut entries, _) =
325 diff_index_to_worktree_with_renames(repo, index, work_tree, options, rename_threshold)?;
326 sort_entries_by_path(&mut entries);
327 Ok(entries)
328}
329
330pub(crate) fn worktree_entries_in(
335 repo: &Repository,
336 index: &mut Index,
337 work_tree: &Path,
338 rules: &Arc<Mutex<WorktreeRules>>,
339 base_tree: Option<&ObjectId>,
340 rename_threshold: u32,
341) -> Result<Vec<DiffEntry>> {
342 let index_matches_base = match (base_tree, index.cache_tree_root.as_ref()) {
343 (Some(base), Some(cached)) => base == cached,
344 (None, None) => true,
345 _ => false,
346 };
347 if index_matches_base && cache_tree_extension_valid(index.cache_tree.as_ref()) {
348 return unstaged_entries_in(repo, index, work_tree, rules, rename_threshold, None);
349 }
350 let config = repo.config()?;
351 let raw = {
352 let rules = rules.lock().unwrap_or_else(|e| e.into_inner());
353 diff_tree_to_worktree_with_git_dir_and_rules(
354 repo.odb(),
355 base_tree,
356 work_tree,
357 repo.git_dir(),
358 index,
359 Some(config.as_ref()),
360 Some(&rules),
361 )?
362 };
363 let mut entries = apply_rename_detection_to_diff_entries(
364 repo.odb(),
365 raw,
366 Some(work_tree),
367 rename_threshold,
368 config.as_ref(),
369 &repo.thread_policy(),
370 )?;
371 sort_entries_by_path(&mut entries);
372 Ok(entries)
373}
374
375pub struct UncommittedDiff<'repo> {
384 repo: &'repo Repository,
385 index: Index,
386 rules: Option<Arc<Mutex<WorktreeRules>>>,
387}
388
389impl<'repo> UncommittedDiff<'repo> {
390 #[must_use]
397 pub fn new(repo: &'repo Repository, index: Index) -> Self {
398 Self {
399 repo,
400 index,
401 rules: None,
402 }
403 }
404
405 #[must_use]
407 pub fn index(&self) -> &Index {
408 &self.index
409 }
410
411 pub fn staged_entries(&self, renames: RenameDetection) -> Result<Vec<DiffEntry>> {
423 staged_entries_in(self.repo, &self.index, rename_threshold(renames), None)
424 }
425
426 pub fn unstaged_entries(&mut self, renames: RenameDetection) -> Result<Vec<DiffEntry>> {
437 let work_tree = self.work_tree()?;
438 let rules = self.rules()?;
439 unstaged_entries_in(
440 self.repo,
441 &mut self.index,
442 work_tree,
443 &rules,
444 rename_threshold(renames),
445 None,
446 )
447 }
448
449 pub fn worktree_entries(
462 &mut self,
463 base_tree: Option<&ObjectId>,
464 renames: RenameDetection,
465 ) -> Result<Vec<DiffEntry>> {
466 let work_tree = self.work_tree()?;
467 let rules = self.rules()?;
468 worktree_entries_in(
469 self.repo,
470 &mut self.index,
471 work_tree,
472 &rules,
473 base_tree,
474 rename_threshold(renames),
475 )
476 }
477
478 pub fn load_worktree_changes(&mut self, entries: Vec<DiffEntry>) -> Result<Vec<FileChange>> {
487 let work_tree = self.work_tree()?;
488 let rules = self.rules()?;
489 let side = WorktreeSide {
490 work_tree,
491 index: &self.index,
492 rules: &rules,
493 };
494 load_changes(self.repo.odb(), entries, Some(&side))
495 }
496
497 fn work_tree(&self) -> Result<&'repo Path> {
498 self.repo.work_tree().ok_or(Error::RequiresWorkTree)
499 }
500
501 fn rules(&mut self) -> Result<Arc<Mutex<WorktreeRules>>> {
502 if let Some(rules) = &self.rules {
503 return Ok(Arc::clone(rules));
504 }
505 let rules = Arc::new(Mutex::new(WorktreeRules::from_repository(
506 self.repo,
507 &self.index,
508 )?));
509 self.rules = Some(Arc::clone(&rules));
510 Ok(rules)
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::repo::InitOptions;
518
519 fn entry(status: DiffStatus, old: Option<&str>, new: Option<&str>) -> DiffEntry {
520 DiffEntry {
521 status,
522 old_path: old.map(str::to_owned),
523 new_path: new.map(str::to_owned),
524 old_mode: FileMode::REGULAR,
525 new_mode: FileMode::REGULAR,
526 old_oid: ObjectId::zero(),
527 new_oid: ObjectId::zero(),
528 score: Some(87),
529 }
530 }
531
532 #[test]
533 fn from_entry_keeps_old_path_only_for_real_renames() {
534 let bytes = || SideBytes::from_bytes(Vec::new());
535 let renamed = FileChange::from_entry(
536 entry(DiffStatus::Renamed, Some("a"), Some("b")),
537 bytes(),
538 bytes(),
539 false,
540 );
541 assert_eq!(renamed.path, "b");
542 assert_eq!(renamed.old_path.as_deref(), Some("a"));
543 assert_eq!(renamed.similarity, Some(87));
544
545 let deleted = FileChange::from_entry(
546 entry(DiffStatus::Deleted, Some("a"), None),
547 bytes(),
548 bytes(),
549 false,
550 );
551 assert_eq!(deleted.path, "a");
552 assert_eq!(deleted.old_path, None);
553 assert_eq!(deleted.similarity, None);
554 }
555
556 #[test]
557 fn binary_change_has_no_hunks_or_line_stat() {
558 let mut change = FileChange::from_entry(
559 entry(DiffStatus::Modified, Some("a"), Some("a")),
560 SideBytes::from_bytes(b"one\n".to_vec()),
561 SideBytes::from_bytes(b"two\n".to_vec()),
562 true,
563 );
564 assert!(change.structured(DiffOptions::default()).hunks.is_empty());
565 assert_eq!(change.line_stat(&DiffOptions::default()), None);
566
567 change.binary = false;
568 assert_eq!(change.structured(DiffOptions::default()).hunks.len(), 1);
569 assert_eq!(
570 change.line_stat(&DiffOptions::default()),
571 Some(LineStat {
572 insertions: 1,
573 deletions: 1,
574 })
575 );
576 }
577
578 #[test]
579 fn worktree_gitlink_line_reads_submodule_head_when_unhashed() {
580 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
581 let mut gitlink = entry(DiffStatus::Added, None, Some("missing-sub"));
582 gitlink.old_mode = FileMode::ABSENT;
583 gitlink.new_mode = FileMode::GITLINK;
584 assert_eq!(worktree_gitlink_line(&gitlink, dir.path()), "");
585
586 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
587 .unwrap_or_else(|e| panic!("oid: {e}"));
588 gitlink.new_oid = oid;
589 assert_eq!(
590 worktree_gitlink_line(&gitlink, dir.path()),
591 format!("Subproject commit {}\n", oid.to_hex())
592 );
593 }
594
595 #[test]
596 fn bare_repository_worktree_methods_require_a_work_tree() {
597 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
598 let init = InitOptions {
599 bare: true,
600 ..InitOptions::default()
601 };
602 let repo = Repository::init(dir.path(), &init).unwrap_or_else(|e| panic!("init: {e}"));
603 let mut diff = UncommittedDiff::new(&repo, Index::default());
604 assert!(diff
605 .staged_entries(RenameDetection::Off)
606 .is_ok_and(|e| e.is_empty()));
607 assert!(matches!(
608 diff.unstaged_entries(RenameDetection::Off),
609 Err(Error::RequiresWorkTree)
610 ));
611 assert!(matches!(
612 diff.worktree_entries(None, RenameDetection::Off),
613 Err(Error::RequiresWorkTree)
614 ));
615 assert!(matches!(
616 diff.load_worktree_changes(Vec::new()),
617 Err(Error::RequiresWorkTree)
618 ));
619 }
620}