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)?,
191 };
192 let (old_bytes, old_binary) = if new_binary {
193 (old_side_without_payload(odb, &entry, worktree)?, false)
194 } else {
195 load_old_side(odb, &entry)?
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 old_side_without_payload(
218 odb: &Odb,
219 entry: &DiffEntry,
220 worktree: Option<&WorktreeSide<'_>>,
221) -> Result<SideBytes> {
222 if entry.old_mode == FileMode::GITLINK {
223 return Ok(SideBytes::from_bytes(
224 gitlink_line(&entry.old_oid).into_bytes(),
225 ));
226 }
227 if entry.old_mode == FileMode::ABSENT {
228 return Ok(SideBytes::empty_with_size(0));
229 }
230 if let Some(side) = worktree {
231 if let Some(path) = entry.old_path.as_deref().or(entry.new_path.as_deref()) {
232 if let Some(ie) = side.index.get(&crate::repo_relpath::storage_bytes(path), 0) {
233 if ie.oid == entry.old_oid && !ie.stat_cache_unset() {
234 return Ok(SideBytes::empty_with_size(u64::from(ie.size)));
235 }
236 }
237 }
238 }
239 if entry.old_oid.is_zero() {
240 return Ok(SideBytes::empty_with_size(0));
241 }
242 let info = odb.read_info(&entry.old_oid)?;
243 Ok(SideBytes::empty_with_size(info.size))
244}
245
246fn load_worktree_new_side(
247 odb: &Odb,
248 entry: &DiffEntry,
249 side: &WorktreeSide<'_>,
250) -> Result<(SideBytes, bool)> {
251 if entry.new_mode == FileMode::GITLINK {
252 let line = worktree_gitlink_line(entry, side.work_tree);
253 return Ok((SideBytes::from_bytes(line.into_bytes()), false));
254 }
255 if entry.new_mode == FileMode::ABSENT {
256 return Ok((SideBytes::from_bytes(Vec::new()), false));
257 }
258 let Some(path) = entry.new_path.as_deref().or(entry.old_path.as_deref()) else {
259 return load_new_side(odb, entry);
260 };
261 let index_entry = side.index.get(&crate::repo_relpath::storage_bytes(path), 0);
264 let rules = side.rules.lock().unwrap_or_else(|e| e.into_inner());
265 load_worktree_side_for_diff(
266 odb,
267 side.work_tree,
268 path,
269 entry.new_mode.bits(),
270 rules.config(),
271 index_entry,
272 Some(&rules),
273 false,
274 )
275}
276
277fn worktree_gitlink_line(entry: &DiffEntry, work_tree: &Path) -> String {
279 let path = entry.new_path.as_deref().or(entry.old_path.as_deref());
280 let head = if entry.new_oid != ObjectId::zero() {
281 Some(entry.new_oid)
282 } else {
283 path.and_then(|p| read_submodule_head_oid(&work_tree.join(p)))
284 };
285 let Some(head) = head.filter(|oid| *oid != ObjectId::zero()) else {
286 return String::new();
287 };
288 let dirty = entry.old_mode == FileMode::GITLINK
289 && path.is_some_and(|p| submodule_porcelain_flags(work_tree, p, entry.old_oid).modified);
290 if dirty {
291 format!("Subproject commit {}-dirty\n", head.to_hex())
292 } else {
293 gitlink_line(&head)
294 }
295}
296
297pub(crate) fn pathspec_restrict_paths_for_scan(
302 repo: &Repository,
303 index: &Index,
304 pathspecs: &[String],
305 base_tree: Option<&ObjectId>,
306) -> Result<Option<Arc<BTreeSet<String>>>> {
307 if pathspecs.is_empty() {
308 return Ok(None);
309 }
310 let base_ctx = repo.pathspec_match_context();
311 let mut paths = crate::porcelain::add::restricted_index_paths_for_pathspecs(
312 repo, index, pathspecs, base_ctx,
313 )?;
314 if let Some(tree) = base_tree {
315 let attr_matcher = if pathspecs
316 .iter()
317 .any(|s| crate::pathspec::pathspec_has_attr_magic(s))
318 {
319 Some(repo.pathspec_matcher(pathspecs)?)
320 } else {
321 None
322 };
323 for (path, mode, _) in super::flatten_tree_entries(repo.odb(), tree)? {
324 if crate::porcelain::add::blob_path_matches_restrict_pathspecs(
325 &path,
326 mode.bits(),
327 pathspecs,
328 base_ctx,
329 attr_matcher.as_ref(),
330 )? {
331 paths.insert(path);
332 }
333 }
334 }
335 Ok(Some(Arc::new(paths)))
336}
337
338fn rename_threshold(renames: RenameDetection) -> u32 {
339 renames
340 .threshold_percent()
341 .unwrap_or(RENAME_DETECTION_DISABLED)
342}
343
344pub(crate) fn staged_entries_in(
348 repo: &Repository,
349 index: &Index,
350 rename_threshold: u32,
351 restrict_to_paths: Option<&BTreeSet<String>>,
352) -> Result<Vec<DiffEntry>> {
353 let head_tree = resolve_head_for_repository(repo)?.tree(repo.odb())?;
354 let config = repo.config()?;
355 let mut entries = match restrict_to_paths.filter(|paths| !paths.is_empty()) {
356 Some(paths) => diff_index_to_tree_for_paths(repo, index, head_tree.as_ref(), paths)?,
357 None => diff_index_to_tree_with_renames(
358 repo.odb(),
359 index,
360 head_tree.as_ref(),
361 false,
362 rename_threshold,
363 config.as_ref(),
364 &repo.thread_policy(),
365 )?,
366 };
367 if let Some(work_tree) = repo.work_tree() {
368 entries.retain(|entry| {
369 if !entry.old_mode.is_gitlink() && !entry.new_mode.is_gitlink() {
370 return true;
371 }
372 let Some(path) = entry.new_path.as_deref().or(entry.old_path.as_deref()) else {
373 return true;
374 };
375 !crate::submodule_ignore::staged_gitlink_omitted_in_diff(
376 config.as_ref(),
377 work_tree,
378 path,
379 )
380 });
381 }
382 sort_entries_by_path(&mut entries);
383 Ok(entries)
384}
385
386pub(crate) fn unstaged_entries_in(
392 repo: &Repository,
393 index: &mut Index,
394 work_tree: &Path,
395 rules: &Arc<Mutex<WorktreeRules>>,
396 rename_threshold: u32,
397 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
398) -> Result<Vec<DiffEntry>> {
399 let config = rules.lock().unwrap_or_else(|e| e.into_inner()).config_arc();
400 let refresh_fsmonitor = restrict_to_paths.is_none() && fsmonitor_enabled(config.as_ref());
401 let options = DiffIndexToWorktreeOptions {
402 repository_git_dir: Some(repo.git_dir().to_path_buf()),
403 index_mtime: index_mtime_for_diff(index, None),
404 config: Some(config),
405 thread_policy: Some(repo.thread_policy()),
406 refresh_index_stat_in_pass: false,
407 size_change_proves_modified: true,
408 refresh_fsmonitor,
409 worktree_rules: Some(Arc::clone(rules)),
410 command_runner: Some(repo.command_runner()),
411 restrict_to_paths,
412 skip_inaccessible_paths: true,
413 diagnostics: Some(repo.diagnostics()),
414 ..DiffIndexToWorktreeOptions::default()
415 };
416 let (mut entries, _) =
417 diff_index_to_worktree_with_renames(repo, index, work_tree, options, rename_threshold)?;
418 sort_entries_by_path(&mut entries);
419 Ok(entries)
420}
421
422pub(crate) fn worktree_entries_in(
427 repo: &Repository,
428 index: &mut Index,
429 work_tree: &Path,
430 rules: &Arc<Mutex<WorktreeRules>>,
431 base_tree: Option<&ObjectId>,
432 rename_threshold: u32,
433 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
434) -> Result<Vec<DiffEntry>> {
435 let index_matches_base = match (base_tree, index.cache_tree_root.as_ref()) {
436 (Some(base), Some(cached)) => base == cached,
437 (None, None) => true,
438 _ => false,
439 };
440 if index_matches_base && cache_tree_extension_valid(index.cache_tree.as_ref()) {
441 return unstaged_entries_in(
442 repo,
443 index,
444 work_tree,
445 rules,
446 rename_threshold,
447 restrict_to_paths,
448 );
449 }
450 let config = repo.config()?;
451 let raw = {
452 let rules = rules.lock().unwrap_or_else(|e| e.into_inner());
453 diff_tree_to_worktree_with_git_dir_and_rules_extras(
454 repo.odb(),
455 base_tree,
456 work_tree,
457 repo.git_dir(),
458 index,
459 Some(config.as_ref()),
460 Some(&rules),
461 DiffTreeToWorktreeExtras {
462 skip_inaccessible_paths: true,
463 diagnostics: Some(repo.diagnostics()),
464 restrict_to_paths: restrict_to_paths.clone(),
465 },
466 )?
467 };
468 let mut entries = apply_rename_detection_to_diff_entries(
469 repo.odb(),
470 raw,
471 Some(work_tree),
472 rename_threshold,
473 config.as_ref(),
474 &repo.thread_policy(),
475 )?;
476 sort_entries_by_path(&mut entries);
477 Ok(entries)
478}
479
480pub struct UncommittedDiff<'repo> {
489 repo: &'repo Repository,
490 index: Index,
491 rules: Option<Arc<Mutex<WorktreeRules>>>,
492}
493
494impl<'repo> UncommittedDiff<'repo> {
495 #[must_use]
502 pub fn new(repo: &'repo Repository, index: Index) -> Self {
503 Self {
504 repo,
505 index,
506 rules: None,
507 }
508 }
509
510 #[must_use]
512 pub fn index(&self) -> &Index {
513 &self.index
514 }
515
516 pub fn staged_entries(&self, renames: RenameDetection) -> Result<Vec<DiffEntry>> {
528 self.staged_entries_for_paths(renames, None)
529 }
530
531 fn staged_entries_for_paths(
532 &self,
533 renames: RenameDetection,
534 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
535 ) -> Result<Vec<DiffEntry>> {
536 staged_entries_in(
537 self.repo,
538 &self.index,
539 rename_threshold(renames),
540 restrict_to_paths.as_deref(),
541 )
542 }
543
544 pub fn unstaged_entries(&mut self, renames: RenameDetection) -> Result<Vec<DiffEntry>> {
558 self.unstaged_entries_for_paths(renames, None)
559 }
560
561 fn unstaged_entries_for_paths(
562 &mut self,
563 renames: RenameDetection,
564 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
565 ) -> Result<Vec<DiffEntry>> {
566 let work_tree = self.work_tree()?;
567 let rules = self.rules()?;
568 unstaged_entries_in(
569 self.repo,
570 &mut self.index,
571 work_tree,
572 &rules,
573 rename_threshold(renames),
574 restrict_to_paths,
575 )
576 }
577
578 pub fn worktree_entries(
591 &mut self,
592 base_tree: Option<&ObjectId>,
593 renames: RenameDetection,
594 ) -> Result<Vec<DiffEntry>> {
595 self.worktree_entries_for_paths(base_tree, renames, None)
596 }
597
598 fn worktree_entries_for_paths(
599 &mut self,
600 base_tree: Option<&ObjectId>,
601 renames: RenameDetection,
602 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
603 ) -> Result<Vec<DiffEntry>> {
604 let work_tree = self.work_tree()?;
605 let rules = self.rules()?;
606 worktree_entries_in(
607 self.repo,
608 &mut self.index,
609 work_tree,
610 &rules,
611 base_tree,
612 rename_threshold(renames),
613 restrict_to_paths,
614 )
615 }
616
617 pub fn entries(
635 &mut self,
636 target: &DiffTarget,
637 pathspecs: &[String],
638 options: &DiffOptions,
639 ) -> Result<super::core::DiffEntriesResult> {
640 let renames = options.renames;
641 let result = match target {
642 DiffTarget::IndexVsHead => {
643 let head_tree = resolve_head_for_repository(self.repo)?.tree(self.repo.odb())?;
644 let restrict = pathspec_restrict_paths_for_scan(
645 self.repo,
646 &self.index,
647 pathspecs,
648 head_tree.as_ref(),
649 )?;
650 super::core::DiffEntriesResult {
651 entries: self.staged_entries_for_paths(renames, restrict)?,
652 inexact_rename_skipped: None,
653 }
654 }
655 DiffTarget::WorktreeVsIndex => {
656 let restrict =
657 pathspec_restrict_paths_for_scan(self.repo, &self.index, pathspecs, None)?;
658 super::core::DiffEntriesResult {
659 entries: self.unstaged_entries_for_paths(renames, restrict)?,
660 inexact_rename_skipped: None,
661 }
662 }
663 DiffTarget::WorktreeVsHead => {
664 let head_tree = resolve_head_for_repository(self.repo)?.tree(self.repo.odb())?;
665 let restrict = pathspec_restrict_paths_for_scan(
666 self.repo,
667 &self.index,
668 pathspecs,
669 head_tree.as_ref(),
670 )?;
671 super::core::DiffEntriesResult {
672 entries: self.worktree_entries_for_paths(
673 head_tree.as_ref(),
674 renames,
675 restrict,
676 )?,
677 inexact_rename_skipped: None,
678 }
679 }
680 DiffTarget::WorktreeVsTree(tree) => {
681 let restrict = pathspec_restrict_paths_for_scan(
682 self.repo,
683 &self.index,
684 pathspecs,
685 Some(tree),
686 )?;
687 super::core::DiffEntriesResult {
688 entries: self.worktree_entries_for_paths(Some(tree), renames, restrict)?,
689 inexact_rename_skipped: None,
690 }
691 }
692 DiffTarget::Commit(_) | DiffTarget::Trees { .. } => {
693 stored_target_entries(self.repo, target, pathspecs, options)?.unwrap_or_default()
694 }
695 };
696 limit_target_entries(self.repo, result, pathspecs, options)
697 }
698
699 pub fn peer_changes<'a>(
720 &mut self,
721 side: DiffSide,
722 pathspecs: &[String],
723 renames: RenameDetection,
724 primary_paths: impl IntoIterator<Item = &'a str>,
725 ) -> Result<Vec<FileChange>> {
726 let peer_target = match side {
727 DiffSide::Staged => DiffTarget::WorktreeVsIndex,
728 DiffSide::Unstaged => DiffTarget::IndexVsHead,
729 DiffSide::Other => return Ok(Vec::new()),
730 };
731 let primary: HashSet<&str> = primary_paths.into_iter().collect();
732 if primary.is_empty() {
733 return Ok(Vec::new());
734 }
735 let options = DiffOptions::default().with_renames(renames);
736 let entries: Vec<DiffEntry> = self
737 .entries(&peer_target, pathspecs, &options)?
738 .entries
739 .into_iter()
740 .filter(|entry| {
741 let display = entry
742 .new_path
743 .as_deref()
744 .or(entry.old_path.as_deref())
745 .unwrap_or("");
746 peer_scope_includes_path(display, entry.old_path.as_deref(), &primary)
747 })
748 .collect();
749 match side {
750 DiffSide::Staged => self.load_worktree_changes(entries),
751 DiffSide::Unstaged | DiffSide::Other => load_object_changes(self.repo, &entries),
752 }
753 }
754
755 pub fn load_worktree_changes(&mut self, entries: Vec<DiffEntry>) -> Result<Vec<FileChange>> {
764 let work_tree = self.work_tree()?;
765 let rules = self.rules()?;
766 let side = WorktreeSide {
767 work_tree,
768 index: &self.index,
769 rules: &rules,
770 };
771 load_changes(self.repo.odb(), entries, Some(&side))
772 }
773
774 fn work_tree(&self) -> Result<&'repo Path> {
775 self.repo.work_tree().ok_or(Error::RequiresWorkTree)
776 }
777
778 fn rules(&mut self) -> Result<Arc<Mutex<WorktreeRules>>> {
779 if let Some(rules) = &self.rules {
780 return Ok(Arc::clone(rules));
781 }
782 let rules = Arc::new(Mutex::new(WorktreeRules::from_repository(
783 self.repo,
784 &self.index,
785 )?));
786 self.rules = Some(Arc::clone(&rules));
787 Ok(rules)
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use super::*;
794 use crate::repo::InitOptions;
795
796 fn entry(status: DiffStatus, old: Option<&str>, new: Option<&str>) -> DiffEntry {
797 DiffEntry {
798 status,
799 old_path: old.map(str::to_owned),
800 new_path: new.map(str::to_owned),
801 old_mode: FileMode::REGULAR,
802 new_mode: FileMode::REGULAR,
803 old_oid: ObjectId::zero(),
804 new_oid: ObjectId::zero(),
805 score: Some(87),
806 }
807 }
808
809 #[test]
810 fn from_entry_keeps_old_path_only_for_real_renames() {
811 let bytes = || SideBytes::from_bytes(Vec::new());
812 let renamed = FileChange::from_entry(
813 entry(DiffStatus::Renamed, Some("a"), Some("b")),
814 bytes(),
815 bytes(),
816 false,
817 );
818 assert_eq!(renamed.path, "b");
819 assert_eq!(renamed.old_path.as_deref(), Some("a"));
820 assert_eq!(renamed.similarity, Some(87));
821
822 let deleted = FileChange::from_entry(
823 entry(DiffStatus::Deleted, Some("a"), None),
824 bytes(),
825 bytes(),
826 false,
827 );
828 assert_eq!(deleted.path, "a");
829 assert_eq!(deleted.old_path, None);
830 assert_eq!(deleted.similarity, None);
831 }
832
833 #[test]
834 fn binary_change_has_no_hunks_or_line_stat() {
835 let mut change = FileChange::from_entry(
836 entry(DiffStatus::Modified, Some("a"), Some("a")),
837 SideBytes::from_bytes(b"one\n".to_vec()),
838 SideBytes::from_bytes(b"two\n".to_vec()),
839 true,
840 );
841 assert!(change.structured(DiffOptions::default()).hunks.is_empty());
842 assert_eq!(change.line_stat(&DiffOptions::default()), None);
843
844 change.binary = false;
845 assert_eq!(change.structured(DiffOptions::default()).hunks.len(), 1);
846 assert_eq!(
847 change.line_stat(&DiffOptions::default()),
848 Some(LineStat {
849 insertions: 1,
850 deletions: 1,
851 })
852 );
853 }
854
855 #[test]
856 fn worktree_gitlink_line_reads_submodule_head_when_unhashed() {
857 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
858 let mut gitlink = entry(DiffStatus::Added, None, Some("missing-sub"));
859 gitlink.old_mode = FileMode::ABSENT;
860 gitlink.new_mode = FileMode::GITLINK;
861 assert_eq!(worktree_gitlink_line(&gitlink, dir.path()), "");
862
863 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
864 .unwrap_or_else(|e| panic!("oid: {e}"));
865 gitlink.new_oid = oid;
866 assert_eq!(
867 worktree_gitlink_line(&gitlink, dir.path()),
868 format!("Subproject commit {}\n", oid.to_hex())
869 );
870 }
871
872 fn staged_repo(files: &[(&str, &[u8])]) -> (tempfile::TempDir, Repository) {
874 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
875 let repo = Repository::init(dir.path(), &InitOptions::default())
876 .unwrap_or_else(|e| panic!("init: {e}"));
877 for (path, bytes) in files {
878 std::fs::write(dir.path().join(path), bytes).unwrap_or_else(|e| panic!("write: {e}"));
879 }
880 crate::porcelain::add::stage(
881 &repo,
882 &crate::porcelain::add::StageOptions::default(),
883 &mut crate::progress::NullProgress,
884 &crate::progress::NeverCancel,
885 )
886 .unwrap_or_else(|e| panic!("stage: {e}"));
887 (dir, repo)
888 }
889
890 fn unstaged(repo: &Repository) -> Vec<DiffEntry> {
891 let index = repo
892 .index_handle()
893 .load()
894 .unwrap_or_else(|e| panic!("index: {e}"));
895 UncommittedDiff::new(repo, index)
896 .unstaged_entries(RenameDetection::Off)
897 .unwrap_or_else(|e| panic!("unstaged: {e}"))
898 }
899
900 #[test]
901 fn unstaged_size_change_is_reported_without_hashing() {
902 let (dir, repo) = staged_repo(&[("grown.txt", b"one\n"), ("same.txt", b"aaaa\n")]);
903 std::fs::write(dir.path().join("grown.txt"), b"one\ntwo\n")
904 .unwrap_or_else(|e| panic!("{e}"));
905 std::fs::write(dir.path().join("same.txt"), b"bbbb\n").unwrap_or_else(|e| panic!("{e}"));
906
907 let metrics = repo.odb().hot_path_metrics();
908 metrics.set_blob_counting(true);
909 metrics.reset_blob_content_reads();
910 let entries = unstaged(&repo);
911 assert_eq!(metrics.blob_content_reads(), 1);
913 assert_eq!(entries.len(), 2);
914 let grown = &entries[0];
915 assert_eq!(grown.path(), "grown.txt");
916 assert_eq!(grown.status, DiffStatus::Modified);
917 assert_eq!(grown.new_oid, ObjectId::zero());
918 let same = &entries[1];
920 assert_eq!(same.path(), "same.txt");
921 assert_eq!(same.status, DiffStatus::Modified);
922
923 let index = repo
924 .index_handle()
925 .load()
926 .unwrap_or_else(|e| panic!("index: {e}"));
927 let changes = UncommittedDiff::new(&repo, index)
928 .load_worktree_changes(entries)
929 .unwrap_or_else(|e| panic!("load: {e}"));
930 assert!(changes[0].content_changed);
931 assert_eq!(
932 changes[0].line_stat(&DiffOptions::default()),
933 Some(LineStat {
934 insertions: 1,
935 deletions: 0,
936 })
937 );
938 }
939
940 #[test]
941 fn binary_worktree_side_skips_reading_the_old_blob() {
942 let (dir, repo) = staged_repo(&[("data.bin", b"old text\n"), ("notes.txt", b"a\n")]);
943 std::fs::write(dir.path().join("data.bin"), b"new\0binary")
944 .unwrap_or_else(|e| panic!("{e}"));
945 std::fs::write(dir.path().join("notes.txt"), b"a\nb\n").unwrap_or_else(|e| panic!("{e}"));
946
947 let entries = unstaged(&repo);
948 let index = repo
949 .index_handle()
950 .load()
951 .unwrap_or_else(|e| panic!("index: {e}"));
952 let mut diff = UncommittedDiff::new(&repo, index);
953 let metrics = repo.odb().hot_path_metrics();
954 metrics.set_blob_counting(true);
955 metrics.reset_blob_content_reads();
956 let binary = diff
957 .load_worktree_changes(vec![entries[0].clone()])
958 .unwrap_or_else(|e| panic!("load: {e}"));
959 assert_eq!(binary[0].path, "data.bin");
960 assert!(binary[0].binary);
961 assert!(binary[0].content_changed);
962 assert_eq!(metrics.blob_content_reads(), 1);
964
965 metrics.reset_blob_content_reads();
966 let text = diff
967 .load_worktree_changes(vec![entries[1].clone()])
968 .unwrap_or_else(|e| panic!("load: {e}"));
969 assert!(!text[0].binary);
970 assert_eq!(text[0].old_bytes.text(), "a\n");
971 assert_eq!(metrics.blob_content_reads(), 2);
972 }
973
974 #[test]
975 fn bare_repository_worktree_methods_require_a_work_tree() {
976 let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
977 let init = InitOptions {
978 bare: true,
979 ..InitOptions::default()
980 };
981 let repo = Repository::init(dir.path(), &init).unwrap_or_else(|e| panic!("init: {e}"));
982 let mut diff = UncommittedDiff::new(&repo, Index::default());
983 assert!(diff
984 .staged_entries(RenameDetection::Off)
985 .is_ok_and(|e| e.is_empty()));
986 assert!(matches!(
987 diff.unstaged_entries(RenameDetection::Off),
988 Err(Error::RequiresWorkTree)
989 ));
990 assert!(matches!(
991 diff.worktree_entries(None, RenameDetection::Off),
992 Err(Error::RequiresWorkTree)
993 ));
994 assert!(matches!(
995 diff.load_worktree_changes(Vec::new()),
996 Err(Error::RequiresWorkTree)
997 ));
998 }
999}