1use std::collections::{BTreeSet, HashSet};
12use std::path::Path;
13use std::sync::{Arc, Mutex};
14
15use crate::diff::structured::{structured_file_diff, SideBytes, StructuredFileDiff};
16use crate::diff::targets::{
17 diff_index_to_tree_for_paths, limit_target_entries, stored_target_entries,
18};
19use crate::diff::tree_content::{gitlink_line, load_new_side, load_old_side};
20use crate::diff::{
21 apply_rename_detection_to_diff_entries, diff_index_to_tree_with_renames,
22 diff_index_to_worktree_with_renames, diff_tree_to_worktree_with_git_dir_and_rules_extras,
23 index_mtime_for_diff, line_change_counts, load_worktree_side_for_diff,
24 peer_scope_includes_path, read_submodule_head_oid, sort_entries_by_path,
25 submodule_porcelain_flags, DiffEntry, DiffIndexToWorktreeOptions, DiffOptions, DiffSide,
26 DiffStatus, DiffTarget, DiffTreeToWorktreeExtras, LineStat, RenameDetection,
27 RENAME_DETECTION_DISABLED,
28};
29use crate::error::{Error, Result};
30use crate::index::write_tree::cache_tree_extension_valid;
31use crate::index::Index;
32use crate::objects::odb::Odb;
33use crate::objects::{FileMode, ObjectId};
34use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
35use crate::repo::Repository;
36use crate::state::resolve_head_for_repository;
37use crate::worktree::fsmonitor::fsmonitor_enabled;
38use crate::worktree::worktree_rules::WorktreeRules;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct FileChange {
43 pub path: String,
45 pub old_path: Option<String>,
47 pub status: DiffStatus,
49 pub old_mode: FileMode,
51 pub new_mode: FileMode,
53 pub old_oid: ObjectId,
55 pub new_oid: ObjectId,
57 pub old_bytes: SideBytes,
59 pub new_bytes: SideBytes,
61 pub binary: bool,
63 pub content_changed: bool,
65 pub similarity: Option<u32>,
67}
68
69impl FileChange {
70 fn from_entry(
71 entry: DiffEntry,
72 old_bytes: SideBytes,
73 new_bytes: SideBytes,
74 binary: bool,
75 ) -> Self {
76 let path = entry
77 .new_path
78 .clone()
79 .or_else(|| entry.old_path.clone())
80 .unwrap_or_default();
81 let similarity = entry.rename_similarity();
82 let old_path = entry.old_path.filter(|old| *old != path);
84 Self {
85 path,
86 old_path,
87 status: entry.status,
88 old_mode: entry.old_mode,
89 new_mode: entry.new_mode,
90 old_oid: entry.old_oid,
91 new_oid: entry.new_oid,
92 old_bytes,
93 new_bytes,
94 binary,
95 content_changed: entry.old_oid != entry.new_oid,
96 similarity,
97 }
98 }
99
100 #[must_use]
108 pub fn structured(&self, options: DiffOptions) -> StructuredFileDiff {
109 if self.binary {
110 return StructuredFileDiff {
111 old_mode: self.old_mode.octal_if_present(),
112 new_mode: self.new_mode.octal_if_present(),
113 old_encoding_lossy: self.old_bytes.encoding_lossy,
114 new_encoding_lossy: self.new_bytes.encoding_lossy,
115 hunks: Vec::new(),
116 };
117 }
118 structured_file_diff(
119 self.old_mode,
120 self.new_mode,
121 &self.old_bytes,
122 &self.new_bytes,
123 options,
124 )
125 }
126
127 #[must_use]
133 pub fn line_stat(&self, options: &DiffOptions) -> Option<LineStat> {
134 if self.binary {
135 return None;
136 }
137 Some(line_change_counts(
138 &self.old_bytes.text(),
139 &self.new_bytes.text(),
140 options,
141 ))
142 }
143}
144
145pub fn load_object_changes(repo: &Repository, entries: &[DiffEntry]) -> Result<Vec<FileChange>> {
160 let config = repo.config()?;
161 let par_opts = repo
162 .thread_policy()
163 .resolve(ParallelOp::DiffContent, config.as_ref());
164 let odb = repo.odb();
165 let est_bytes = entries.len().saturating_mul(8192);
166 try_par_map(entries, par_opts, est_bytes, |entry| {
167 load_change(odb, entry.clone(), None)
168 })
169 .map_err(|ParallelHashError::Task(err)| err)
170}
171
172pub(crate) struct WorktreeSide<'a> {
174 pub(crate) work_tree: &'a Path,
175 pub(crate) index: &'a Index,
176 pub(crate) rules: &'a Mutex<WorktreeRules>,
177}
178
179pub(crate) fn load_change(
184 odb: &Odb,
185 entry: DiffEntry,
186 worktree: Option<&WorktreeSide<'_>>,
187) -> Result<FileChange> {
188 let (new_bytes, new_binary) = match worktree {
189 Some(side) => load_worktree_new_side(odb, &entry, side)?,
190 None => load_new_side(odb, &entry, None)?,
191 };
192 let (old_bytes, old_binary) = if new_binary {
193 (SideBytes::from_bytes(Vec::new()), false)
194 } else {
195 load_old_side(odb, &entry, None)?
196 };
197 Ok(FileChange::from_entry(
198 entry,
199 old_bytes,
200 new_bytes,
201 old_binary || new_binary,
202 ))
203}
204
205pub(crate) fn load_changes(
207 odb: &Odb,
208 entries: Vec<DiffEntry>,
209 worktree: Option<&WorktreeSide<'_>>,
210) -> Result<Vec<FileChange>> {
211 entries
212 .into_iter()
213 .map(|entry| load_change(odb, entry, worktree))
214 .collect()
215}
216
217fn load_worktree_new_side(
218 odb: &Odb,
219 entry: &DiffEntry,
220 side: &WorktreeSide<'_>,
221) -> Result<(SideBytes, bool)> {
222 if entry.new_mode == FileMode::GITLINK {
223 let line = worktree_gitlink_line(entry, side.work_tree);
224 return Ok((SideBytes::from_bytes(line.into_bytes()), false));
225 }
226 if entry.new_mode == FileMode::ABSENT {
227 return Ok((SideBytes::from_bytes(Vec::new()), false));
228 }
229 let Some(path) = entry.new_path.as_deref().or(entry.old_path.as_deref()) else {
230 return load_new_side(odb, entry, None);
231 };
232 let index_entry = side.index.get(&crate::repo_relpath::storage_bytes(path), 0);
235 let rules = side.rules.lock().unwrap_or_else(|e| e.into_inner());
236 load_worktree_side_for_diff(
237 odb,
238 side.work_tree,
239 path,
240 entry.new_mode.bits(),
241 rules.config(),
242 index_entry,
243 Some(&rules),
244 false,
245 )
246}
247
248fn worktree_gitlink_line(entry: &DiffEntry, work_tree: &Path) -> String {
250 let path = entry.new_path.as_deref().or(entry.old_path.as_deref());
251 let head = if entry.new_oid != ObjectId::zero() {
252 Some(entry.new_oid)
253 } else {
254 path.and_then(|p| read_submodule_head_oid(&work_tree.join(p)))
255 };
256 let Some(head) = head.filter(|oid| *oid != ObjectId::zero()) else {
257 return String::new();
258 };
259 let dirty = entry.old_mode == FileMode::GITLINK
260 && path.is_some_and(|p| submodule_porcelain_flags(work_tree, p, entry.old_oid).modified);
261 if dirty {
262 format!("Subproject commit {}-dirty\n", head.to_hex())
263 } else {
264 gitlink_line(&head)
265 }
266}
267
268pub(crate) fn pathspec_restrict_paths_for_scan(
273 repo: &Repository,
274 index: &Index,
275 pathspecs: &[String],
276 base_tree: Option<&ObjectId>,
277) -> Result<Option<Arc<BTreeSet<String>>>> {
278 if pathspecs.is_empty() {
279 return Ok(None);
280 }
281 let base_ctx = repo.pathspec_match_context();
282 let mut paths = crate::porcelain::add::restricted_index_paths_for_pathspecs(
283 repo, index, pathspecs, base_ctx,
284 )?;
285 if let Some(tree) = base_tree {
286 let attr_matcher = if pathspecs
287 .iter()
288 .any(|s| crate::pathspec::pathspec_has_attr_magic(s))
289 {
290 Some(repo.pathspec_matcher(pathspecs)?)
291 } else {
292 None
293 };
294 for (path, mode, _) in super::flatten_tree_entries(repo.odb(), tree)? {
295 if crate::porcelain::add::blob_path_matches_restrict_pathspecs(
296 &path,
297 mode.bits(),
298 pathspecs,
299 base_ctx,
300 attr_matcher.as_ref(),
301 )? {
302 paths.insert(path);
303 }
304 }
305 }
306 Ok(Some(Arc::new(paths)))
307}
308
309fn rename_threshold(renames: RenameDetection) -> u32 {
310 renames
311 .threshold_percent()
312 .unwrap_or(RENAME_DETECTION_DISABLED)
313}
314
315pub(crate) fn staged_entries_in(
319 repo: &Repository,
320 index: &Index,
321 rename_threshold: u32,
322 restrict_to_paths: Option<&BTreeSet<String>>,
323) -> Result<Vec<DiffEntry>> {
324 let head_tree = resolve_head_for_repository(repo)?.tree(repo.odb())?;
325 let mut entries = match restrict_to_paths.filter(|paths| !paths.is_empty()) {
326 Some(paths) => diff_index_to_tree_for_paths(repo, index, head_tree.as_ref(), paths)?,
327 None => {
328 let config = repo.config()?;
329 diff_index_to_tree_with_renames(
330 repo.odb(),
331 index,
332 head_tree.as_ref(),
333 false,
334 rename_threshold,
335 config.as_ref(),
336 &repo.thread_policy(),
337 )?
338 }
339 };
340 sort_entries_by_path(&mut entries);
341 Ok(entries)
342}
343
344pub(crate) fn unstaged_entries_in(
350 repo: &Repository,
351 index: &mut Index,
352 work_tree: &Path,
353 rules: &Arc<Mutex<WorktreeRules>>,
354 rename_threshold: u32,
355 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
356) -> Result<Vec<DiffEntry>> {
357 let config = rules.lock().unwrap_or_else(|e| e.into_inner()).config_arc();
358 let refresh_fsmonitor = restrict_to_paths.is_none() && fsmonitor_enabled(config.as_ref());
359 let options = DiffIndexToWorktreeOptions {
360 repository_git_dir: Some(repo.git_dir().to_path_buf()),
361 index_mtime: index_mtime_for_diff(index, None),
362 config: Some(config),
363 thread_policy: Some(repo.thread_policy()),
364 refresh_index_stat_in_pass: false,
365 size_change_proves_modified: true,
366 refresh_fsmonitor,
367 worktree_rules: Some(Arc::clone(rules)),
368 command_runner: Some(repo.command_runner()),
369 restrict_to_paths,
370 skip_inaccessible_paths: true,
371 diagnostics: Some(repo.diagnostics()),
372 ..DiffIndexToWorktreeOptions::default()
373 };
374 let (mut entries, _) =
375 diff_index_to_worktree_with_renames(repo, index, work_tree, options, rename_threshold)?;
376 sort_entries_by_path(&mut entries);
377 Ok(entries)
378}
379
380pub(crate) fn worktree_entries_in(
385 repo: &Repository,
386 index: &mut Index,
387 work_tree: &Path,
388 rules: &Arc<Mutex<WorktreeRules>>,
389 base_tree: Option<&ObjectId>,
390 rename_threshold: u32,
391 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
392) -> Result<Vec<DiffEntry>> {
393 let index_matches_base = match (base_tree, index.cache_tree_root.as_ref()) {
394 (Some(base), Some(cached)) => base == cached,
395 (None, None) => true,
396 _ => false,
397 };
398 if index_matches_base && cache_tree_extension_valid(index.cache_tree.as_ref()) {
399 return unstaged_entries_in(
400 repo,
401 index,
402 work_tree,
403 rules,
404 rename_threshold,
405 restrict_to_paths,
406 );
407 }
408 let config = repo.config()?;
409 let raw = {
410 let rules = rules.lock().unwrap_or_else(|e| e.into_inner());
411 diff_tree_to_worktree_with_git_dir_and_rules_extras(
412 repo.odb(),
413 base_tree,
414 work_tree,
415 repo.git_dir(),
416 index,
417 Some(config.as_ref()),
418 Some(&rules),
419 DiffTreeToWorktreeExtras {
420 skip_inaccessible_paths: true,
421 diagnostics: Some(repo.diagnostics()),
422 restrict_to_paths: restrict_to_paths.clone(),
423 },
424 )?
425 };
426 let mut entries = apply_rename_detection_to_diff_entries(
427 repo.odb(),
428 raw,
429 Some(work_tree),
430 rename_threshold,
431 config.as_ref(),
432 &repo.thread_policy(),
433 )?;
434 sort_entries_by_path(&mut entries);
435 Ok(entries)
436}
437
438pub struct UncommittedDiff<'repo> {
447 repo: &'repo Repository,
448 index: Index,
449 rules: Option<Arc<Mutex<WorktreeRules>>>,
450}
451
452impl<'repo> UncommittedDiff<'repo> {
453 #[must_use]
460 pub fn new(repo: &'repo Repository, index: Index) -> Self {
461 Self {
462 repo,
463 index,
464 rules: None,
465 }
466 }
467
468 #[must_use]
470 pub fn index(&self) -> &Index {
471 &self.index
472 }
473
474 pub fn staged_entries(&self, renames: RenameDetection) -> Result<Vec<DiffEntry>> {
486 self.staged_entries_for_paths(renames, None)
487 }
488
489 fn staged_entries_for_paths(
490 &self,
491 renames: RenameDetection,
492 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
493 ) -> Result<Vec<DiffEntry>> {
494 staged_entries_in(
495 self.repo,
496 &self.index,
497 rename_threshold(renames),
498 restrict_to_paths.as_deref(),
499 )
500 }
501
502 pub fn unstaged_entries(&mut self, renames: RenameDetection) -> Result<Vec<DiffEntry>> {
516 self.unstaged_entries_for_paths(renames, None)
517 }
518
519 fn unstaged_entries_for_paths(
520 &mut self,
521 renames: RenameDetection,
522 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
523 ) -> Result<Vec<DiffEntry>> {
524 let work_tree = self.work_tree()?;
525 let rules = self.rules()?;
526 unstaged_entries_in(
527 self.repo,
528 &mut self.index,
529 work_tree,
530 &rules,
531 rename_threshold(renames),
532 restrict_to_paths,
533 )
534 }
535
536 pub fn worktree_entries(
549 &mut self,
550 base_tree: Option<&ObjectId>,
551 renames: RenameDetection,
552 ) -> Result<Vec<DiffEntry>> {
553 self.worktree_entries_for_paths(base_tree, renames, None)
554 }
555
556 fn worktree_entries_for_paths(
557 &mut self,
558 base_tree: Option<&ObjectId>,
559 renames: RenameDetection,
560 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
561 ) -> Result<Vec<DiffEntry>> {
562 let work_tree = self.work_tree()?;
563 let rules = self.rules()?;
564 worktree_entries_in(
565 self.repo,
566 &mut self.index,
567 work_tree,
568 &rules,
569 base_tree,
570 rename_threshold(renames),
571 restrict_to_paths,
572 )
573 }
574
575 pub fn entries(
593 &mut self,
594 target: &DiffTarget,
595 pathspecs: &[String],
596 options: &DiffOptions,
597 ) -> Result<super::core::DiffEntriesResult> {
598 let renames = options.renames;
599 let result = match target {
600 DiffTarget::IndexVsHead => {
601 let head_tree = resolve_head_for_repository(self.repo)?.tree(self.repo.odb())?;
602 let restrict = pathspec_restrict_paths_for_scan(
603 self.repo,
604 &self.index,
605 pathspecs,
606 head_tree.as_ref(),
607 )?;
608 super::core::DiffEntriesResult {
609 entries: self.staged_entries_for_paths(renames, restrict)?,
610 inexact_rename_skipped: None,
611 }
612 }
613 DiffTarget::WorktreeVsIndex => {
614 let restrict =
615 pathspec_restrict_paths_for_scan(self.repo, &self.index, pathspecs, None)?;
616 super::core::DiffEntriesResult {
617 entries: self.unstaged_entries_for_paths(renames, restrict)?,
618 inexact_rename_skipped: None,
619 }
620 }
621 DiffTarget::WorktreeVsHead => {
622 let head_tree = resolve_head_for_repository(self.repo)?.tree(self.repo.odb())?;
623 let restrict = pathspec_restrict_paths_for_scan(
624 self.repo,
625 &self.index,
626 pathspecs,
627 head_tree.as_ref(),
628 )?;
629 super::core::DiffEntriesResult {
630 entries: self.worktree_entries_for_paths(
631 head_tree.as_ref(),
632 renames,
633 restrict,
634 )?,
635 inexact_rename_skipped: None,
636 }
637 }
638 DiffTarget::WorktreeVsTree(tree) => {
639 let restrict = pathspec_restrict_paths_for_scan(
640 self.repo,
641 &self.index,
642 pathspecs,
643 Some(tree),
644 )?;
645 super::core::DiffEntriesResult {
646 entries: self.worktree_entries_for_paths(Some(tree), renames, restrict)?,
647 inexact_rename_skipped: None,
648 }
649 }
650 DiffTarget::Commit(_) | DiffTarget::Trees { .. } => {
651 stored_target_entries(self.repo, target, pathspecs, options)?.unwrap_or_default()
652 }
653 };
654 limit_target_entries(self.repo, result, pathspecs, options)
655 }
656
657 pub fn peer_changes<'a>(
678 &mut self,
679 side: DiffSide,
680 pathspecs: &[String],
681 renames: RenameDetection,
682 primary_paths: impl IntoIterator<Item = &'a str>,
683 ) -> Result<Vec<FileChange>> {
684 let peer_target = match side {
685 DiffSide::Staged => DiffTarget::WorktreeVsIndex,
686 DiffSide::Unstaged => DiffTarget::IndexVsHead,
687 DiffSide::Other => return Ok(Vec::new()),
688 };
689 let primary: HashSet<&str> = primary_paths.into_iter().collect();
690 if primary.is_empty() {
691 return Ok(Vec::new());
692 }
693 let options = DiffOptions::default().with_renames(renames);
694 let entries: Vec<DiffEntry> = self
695 .entries(&peer_target, pathspecs, &options)?
696 .entries
697 .into_iter()
698 .filter(|entry| {
699 let display = entry
700 .new_path
701 .as_deref()
702 .or(entry.old_path.as_deref())
703 .unwrap_or("");
704 peer_scope_includes_path(display, entry.old_path.as_deref(), &primary)
705 })
706 .collect();
707 match side {
708 DiffSide::Staged => self.load_worktree_changes(entries),
709 DiffSide::Unstaged | DiffSide::Other => load_object_changes(self.repo, &entries),
710 }
711 }
712
713 pub fn load_worktree_changes(&mut self, entries: Vec<DiffEntry>) -> Result<Vec<FileChange>> {
722 let work_tree = self.work_tree()?;
723 let rules = self.rules()?;
724 let side = WorktreeSide {
725 work_tree,
726 index: &self.index,
727 rules: &rules,
728 };
729 load_changes(self.repo.odb(), entries, Some(&side))
730 }
731
732 fn work_tree(&self) -> Result<&'repo Path> {
733 self.repo.work_tree().ok_or(Error::RequiresWorkTree)
734 }
735
736 fn rules(&mut self) -> Result<Arc<Mutex<WorktreeRules>>> {
737 if let Some(rules) = &self.rules {
738 return Ok(Arc::clone(rules));
739 }
740 let rules = Arc::new(Mutex::new(WorktreeRules::from_repository(
741 self.repo,
742 &self.index,
743 )?));
744 self.rules = Some(Arc::clone(&rules));
745 Ok(rules)
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752 use crate::repo::InitOptions;
753
754 fn entry(status: DiffStatus, old: Option<&str>, new: Option<&str>) -> DiffEntry {
755 DiffEntry {
756 status,
757 old_path: old.map(str::to_owned),
758 new_path: new.map(str::to_owned),
759 old_mode: FileMode::REGULAR,
760 new_mode: FileMode::REGULAR,
761 old_oid: ObjectId::zero(),
762 new_oid: ObjectId::zero(),
763 score: Some(87),
764 }
765 }
766
767 #[test]
768 fn from_entry_keeps_old_path_only_for_real_renames() {
769 let bytes = || SideBytes::from_bytes(Vec::new());
770 let renamed = FileChange::from_entry(
771 entry(DiffStatus::Renamed, Some("a"), Some("b")),
772 bytes(),
773 bytes(),
774 false,
775 );
776 assert_eq!(renamed.path, "b");
777 assert_eq!(renamed.old_path.as_deref(), Some("a"));
778 assert_eq!(renamed.similarity, Some(87));
779
780 let deleted = FileChange::from_entry(
781 entry(DiffStatus::Deleted, Some("a"), None),
782 bytes(),
783 bytes(),
784 false,
785 );
786 assert_eq!(deleted.path, "a");
787 assert_eq!(deleted.old_path, None);
788 assert_eq!(deleted.similarity, None);
789 }
790
791 #[test]
792 fn binary_change_has_no_hunks_or_line_stat() {
793 let mut change = FileChange::from_entry(
794 entry(DiffStatus::Modified, Some("a"), Some("a")),
795 SideBytes::from_bytes(b"one\n".to_vec()),
796 SideBytes::from_bytes(b"two\n".to_vec()),
797 true,
798 );
799 assert!(change.structured(DiffOptions::default()).hunks.is_empty());
800 assert_eq!(change.line_stat(&DiffOptions::default()), None);
801
802 change.binary = false;
803 assert_eq!(change.structured(DiffOptions::default()).hunks.len(), 1);
804 assert_eq!(
805 change.line_stat(&DiffOptions::default()),
806 Some(LineStat {
807 insertions: 1,
808 deletions: 1,
809 })
810 );
811 }
812
813 #[test]
814 fn worktree_gitlink_line_reads_submodule_head_when_unhashed() {
815 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
816 let mut gitlink = entry(DiffStatus::Added, None, Some("missing-sub"));
817 gitlink.old_mode = FileMode::ABSENT;
818 gitlink.new_mode = FileMode::GITLINK;
819 assert_eq!(worktree_gitlink_line(&gitlink, dir.path()), "");
820
821 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
822 .unwrap_or_else(|e| panic!("oid: {e}"));
823 gitlink.new_oid = oid;
824 assert_eq!(
825 worktree_gitlink_line(&gitlink, dir.path()),
826 format!("Subproject commit {}\n", oid.to_hex())
827 );
828 }
829
830 fn staged_repo(files: &[(&str, &[u8])]) -> (tempfile::TempDir, Repository) {
832 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
833 let repo = Repository::init(dir.path(), &InitOptions::default())
834 .unwrap_or_else(|e| panic!("init: {e}"));
835 for (path, bytes) in files {
836 std::fs::write(dir.path().join(path), bytes).unwrap_or_else(|e| panic!("write: {e}"));
837 }
838 crate::porcelain::add::stage(
839 &repo,
840 &crate::porcelain::add::StageOptions::default(),
841 &mut crate::progress::NullProgress,
842 &crate::progress::NeverCancel,
843 )
844 .unwrap_or_else(|e| panic!("stage: {e}"));
845 (dir, repo)
846 }
847
848 fn unstaged(repo: &Repository) -> Vec<DiffEntry> {
849 let index = repo
850 .index_handle()
851 .load()
852 .unwrap_or_else(|e| panic!("index: {e}"));
853 UncommittedDiff::new(repo, index)
854 .unstaged_entries(RenameDetection::Off)
855 .unwrap_or_else(|e| panic!("unstaged: {e}"))
856 }
857
858 #[test]
859 fn unstaged_size_change_is_reported_without_hashing() {
860 let (dir, repo) = staged_repo(&[("grown.txt", b"one\n"), ("same.txt", b"aaaa\n")]);
861 std::fs::write(dir.path().join("grown.txt"), b"one\ntwo\n")
862 .unwrap_or_else(|e| panic!("{e}"));
863 std::fs::write(dir.path().join("same.txt"), b"bbbb\n").unwrap_or_else(|e| panic!("{e}"));
864
865 let metrics = repo.odb().hot_path_metrics();
866 metrics.set_blob_counting(true);
867 metrics.reset_blob_content_reads();
868 let entries = unstaged(&repo);
869 assert_eq!(metrics.blob_content_reads(), 1);
871 assert_eq!(entries.len(), 2);
872 let grown = &entries[0];
873 assert_eq!(grown.path(), "grown.txt");
874 assert_eq!(grown.status, DiffStatus::Modified);
875 assert_eq!(grown.new_oid, ObjectId::zero());
876 let same = &entries[1];
878 assert_eq!(same.path(), "same.txt");
879 assert_eq!(same.status, DiffStatus::Modified);
880
881 let index = repo
882 .index_handle()
883 .load()
884 .unwrap_or_else(|e| panic!("index: {e}"));
885 let changes = UncommittedDiff::new(&repo, index)
886 .load_worktree_changes(entries)
887 .unwrap_or_else(|e| panic!("load: {e}"));
888 assert!(changes[0].content_changed);
889 assert_eq!(
890 changes[0].line_stat(&DiffOptions::default()),
891 Some(LineStat {
892 insertions: 1,
893 deletions: 0,
894 })
895 );
896 }
897
898 #[test]
899 fn binary_worktree_side_skips_reading_the_old_blob() {
900 let (dir, repo) = staged_repo(&[("data.bin", b"old text\n"), ("notes.txt", b"a\n")]);
901 std::fs::write(dir.path().join("data.bin"), b"new\0binary")
902 .unwrap_or_else(|e| panic!("{e}"));
903 std::fs::write(dir.path().join("notes.txt"), b"a\nb\n").unwrap_or_else(|e| panic!("{e}"));
904
905 let entries = unstaged(&repo);
906 let index = repo
907 .index_handle()
908 .load()
909 .unwrap_or_else(|e| panic!("index: {e}"));
910 let mut diff = UncommittedDiff::new(&repo, index);
911 let metrics = repo.odb().hot_path_metrics();
912 metrics.set_blob_counting(true);
913 metrics.reset_blob_content_reads();
914 let binary = diff
915 .load_worktree_changes(vec![entries[0].clone()])
916 .unwrap_or_else(|e| panic!("load: {e}"));
917 assert_eq!(binary[0].path, "data.bin");
918 assert!(binary[0].binary);
919 assert!(binary[0].content_changed);
920 assert_eq!(metrics.blob_content_reads(), 1);
922
923 metrics.reset_blob_content_reads();
924 let text = diff
925 .load_worktree_changes(vec![entries[1].clone()])
926 .unwrap_or_else(|e| panic!("load: {e}"));
927 assert!(!text[0].binary);
928 assert_eq!(text[0].old_bytes.text(), "a\n");
929 assert_eq!(metrics.blob_content_reads(), 2);
930 }
931
932 #[test]
933 fn bare_repository_worktree_methods_require_a_work_tree() {
934 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
935 let init = InitOptions {
936 bare: true,
937 ..InitOptions::default()
938 };
939 let repo = Repository::init(dir.path(), &init).unwrap_or_else(|e| panic!("init: {e}"));
940 let mut diff = UncommittedDiff::new(&repo, Index::default());
941 assert!(diff
942 .staged_entries(RenameDetection::Off)
943 .is_ok_and(|e| e.is_empty()));
944 assert!(matches!(
945 diff.unstaged_entries(RenameDetection::Off),
946 Err(Error::RequiresWorkTree)
947 ));
948 assert!(matches!(
949 diff.worktree_entries(None, RenameDetection::Off),
950 Err(Error::RequiresWorkTree)
951 ));
952 assert!(matches!(
953 diff.load_worktree_changes(Vec::new()),
954 Err(Error::RequiresWorkTree)
955 ));
956 }
957}