1use std::collections::BTreeSet;
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6
7use thiserror::Error;
8
9use crate::config::ConfigSet;
10use crate::diff::changes::{
11 load_changes, load_object_changes, pathspec_restrict_paths_for_scan, staged_entries_in,
12 unstaged_entries_in, worktree_entries_in, FileChange, WorktreeSide,
13};
14use crate::diff::detail::LineStat;
15use crate::diff::structured::StructuredHunk;
16use crate::diff::{
17 apply_diff_filter, sort_entries_by_path, tree_diff_structured_parallel, DiffEntry, DiffOptions,
18 DiffStatus, TreeDiffStructured,
19};
20use crate::error::{Error, PathspecResolveError, Result};
21use crate::index::Index;
22use crate::index::MODE_TREE;
23use crate::objects::FileMode;
24use crate::objects::ObjectId;
25use crate::parallel::{par_map, ParallelOp, ThreadPolicy};
26use crate::pathspec::{exact_literal_pathspec_set, PathspecMatchContext, PathspecMatcher};
27use crate::porcelain::pathspec_match::{
28 ensure_pathspec_matches_known_in_repository, pathspec_matches_known_in_repository,
29};
30use crate::repo::Repository;
31use crate::rev_parse::peel_tags_to_commit;
32use crate::revwalk::merge_base::{merge_base_for_diff_two_commits, MergeBaseForDiffError};
33use crate::revwalk::rev_parse::{
34 resolve_revision, resolve_revision_for_range_end, split_double_dot_range,
35 split_triple_dot_range,
36};
37use crate::state::resolve_head_for_repository;
38use crate::worktree::pathspec::resolve_pathspec;
39use crate::worktree::worktree_rules::WorktreeRules;
40
41#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum DiffTarget {
45 WorktreeVsHead,
47 WorktreeVsTree(ObjectId),
49 IndexVsHead,
51 WorktreeVsIndex,
53 Commit(ObjectId),
55 Trees {
57 old: Option<ObjectId>,
59 new: ObjectId,
61 },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct FileDiff {
71 pub path: String,
73 pub old_path: Option<String>,
75 pub status: DiffStatus,
77 pub old_mode: Option<String>,
79 pub new_mode: Option<String>,
81 pub old_oid: ObjectId,
83 pub new_oid: ObjectId,
85 pub binary: bool,
87 pub old_size: u64,
89 pub new_size: u64,
91 pub content_changed: bool,
93 pub old_encoding_lossy: bool,
95 pub new_encoding_lossy: bool,
97 pub hunks: Vec<StructuredHunk>,
99 pub line_stat: Option<LineStat>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct DiffArguments {
106 pub target: DiffTarget,
108 pub pathspecs: Vec<String>,
110}
111
112#[derive(Debug, Clone, Default, PartialEq, Eq)]
116pub struct DiffRequest {
117 pub staged: bool,
119 pub unstaged: bool,
121 pub against: Option<String>,
123 pub positionals: Vec<String>,
125 pub paths: Vec<String>,
127 pub pathspec_file: Option<Vec<String>>,
132}
133
134#[non_exhaustive]
136#[derive(Debug, Error)]
137pub enum ResolveDiffArgumentsError {
138 #[error("--staged and --unstaged cannot be combined")]
140 StagedAndUnstaged,
141
142 #[error("--staged cannot be combined with revision arguments or --against")]
144 StagedWithRevisions,
145
146 #[error("--unstaged cannot be combined with revision arguments or --against")]
148 UnstagedWithRevisions,
149
150 #[error("at most two commits, or one two-dot range such as A..B")]
152 TooManyRevisions,
153
154 #[error("--against compares the worktree to a ref and takes no commit arguments")]
156 AgainstWithCommits,
157
158 #[error(
160 "a two-dot or three-dot range uses the other positional as a path limit; pass paths after `--`"
161 )]
162 RangeWithSecondRevision,
163
164 #[error("grit diff needs a working tree")]
166 NoWorkTree,
167
168 #[error("could not resolve '{spec}'")]
170 UnresolvedRevision {
171 spec: String,
173 #[source]
175 source: Box<Error>,
176 },
177
178 #[error("object {oid} does not name a commit")]
180 NotACommit {
181 oid: ObjectId,
183 },
184
185 #[error("no merge base for that revision range")]
187 NoMergeBase,
188
189 #[error("multiple merge bases for that revision range")]
191 MultipleMergeBases,
192
193 #[error(transparent)]
195 Pathspec(#[from] PathspecResolveError),
196
197 #[error(transparent)]
200 Library(Box<Error>),
201}
202
203impl From<Error> for ResolveDiffArgumentsError {
204 fn from(err: Error) -> Self {
205 Self::Library(Box::new(err))
206 }
207}
208
209type ResolveResult<T> = std::result::Result<T, ResolveDiffArgumentsError>;
210
211pub fn resolve_diff_arguments(
230 repo: &Repository,
231 cwd_prefix: Option<&str>,
232 staged: bool,
233 unstaged: bool,
234 against: Option<&str>,
235 positionals: &[String],
236 explicit_paths: &[String],
237) -> ResolveResult<DiffArguments> {
238 let request = DiffRequest {
239 staged,
240 unstaged,
241 against: against.map(str::to_owned),
242 positionals: positionals.to_vec(),
243 paths: explicit_paths.to_vec(),
244 pathspec_file: None,
245 };
246 resolve_diff_request(repo, cwd_prefix, &request)
247}
248
249pub fn resolve_diff_request(
288 repo: &Repository,
289 cwd_prefix: Option<&str>,
290 request: &DiffRequest,
291) -> ResolveResult<DiffArguments> {
292 let arguments = resolve_diff_request_in(repo, cwd_prefix, request)?;
293 let reads_worktree = matches!(
294 arguments.target,
295 DiffTarget::WorktreeVsHead | DiffTarget::WorktreeVsTree(_) | DiffTarget::WorktreeVsIndex
296 );
297 if reads_worktree && repo.work_tree().is_none() {
298 return Err(ResolveDiffArgumentsError::NoWorkTree);
299 }
300 Ok(arguments)
301}
302
303fn resolve_diff_request_in(
304 repo: &Repository,
305 cwd_prefix: Option<&str>,
306 request: &DiffRequest,
307) -> ResolveResult<DiffArguments> {
308 let (commits, path_args) = disambiguate_diff_revision_and_paths(
309 repo,
310 cwd_prefix,
311 request.positionals.clone(),
312 request.paths.clone(),
313 );
314 let file_paths = request.pathspec_file.as_deref().unwrap_or_default();
315 let path_tokens = [file_paths, path_args.as_slice()].concat();
316 if (request.pathspec_file.is_some() || !request.paths.is_empty())
317 && (path_tokens.is_empty() || path_tokens.iter().any(String::is_empty))
318 {
319 return Err(PathspecResolveError::EmptyPathspec.into());
320 }
321 let against = request.against.as_deref();
322
323 if request.staged && request.unstaged {
324 return Err(ResolveDiffArgumentsError::StagedAndUnstaged);
325 }
326 if request.staged || request.unstaged {
327 if !commits.is_empty() || against.is_some() {
328 return Err(if request.staged {
329 ResolveDiffArgumentsError::StagedWithRevisions
330 } else {
331 ResolveDiffArgumentsError::UnstagedWithRevisions
332 });
333 }
334 return Ok(DiffArguments {
335 target: if request.staged {
336 DiffTarget::IndexVsHead
337 } else {
338 DiffTarget::WorktreeVsIndex
339 },
340 pathspecs: resolve_worktree_diff_pathspecs(repo, cwd_prefix, &path_tokens)?,
341 });
342 }
343 if commits.len() > 2 {
344 return Err(ResolveDiffArgumentsError::TooManyRevisions);
345 }
346 if against.is_some() && !commits.is_empty() {
347 return Err(ResolveDiffArgumentsError::AgainstWithCommits);
348 }
349
350 let trees = |old: ObjectId, new: ObjectId, pathspecs: Vec<String>| DiffArguments {
351 target: DiffTarget::Trees {
352 old: Some(old),
353 new,
354 },
355 pathspecs,
356 };
357 match commits.as_slice() {
358 [] => {
359 let pathspecs = resolve_worktree_diff_pathspecs(repo, cwd_prefix, &path_tokens)?;
360 let target = match against {
361 Some(spec) => DiffTarget::WorktreeVsTree(
362 repo.odb().commit_tree(&resolve_diff_commit(repo, spec)?)?,
363 ),
364 None => DiffTarget::WorktreeVsHead,
365 };
366 Ok(DiffArguments { target, pathspecs })
367 }
368 [spec] => {
369 if let Some((base, tip)) = parse_commit_range(repo, spec)? {
370 let pathspecs = resolve_commit_diff_pathspecs(repo, cwd_prefix, &path_tokens)?;
371 return Ok(trees(base, tip, pathspecs));
372 }
373 match resolve_diff_commit(repo, spec) {
374 Ok(oid) => Ok(DiffArguments {
375 target: DiffTarget::Commit(oid),
376 pathspecs: resolve_commit_diff_pathspecs(repo, cwd_prefix, &path_tokens)?,
377 }),
378 Err(_) if path_tokens.is_empty() => Ok(DiffArguments {
380 target: DiffTarget::WorktreeVsIndex,
381 pathspecs: resolve_worktree_diff_pathspecs(
382 repo,
383 cwd_prefix,
384 std::slice::from_ref(spec),
385 )?,
386 }),
387 Err(err) => Err(err),
388 }
389 }
390 [first, second] => {
391 if let Some((base, tip)) = parse_commit_range(repo, first)? {
392 if !token_is_known_worktree_pathspec(repo, cwd_prefix, second) {
393 resolve_diff_commit(repo, second)?;
394 return Err(ResolveDiffArgumentsError::RangeWithSecondRevision);
395 }
396 let range_paths = [file_paths, std::slice::from_ref(second)].concat();
397 let pathspecs = resolve_commit_diff_pathspecs(repo, cwd_prefix, &range_paths)?;
398 return Ok(trees(base, tip, pathspecs));
399 }
400 let base = resolve_diff_commit(repo, first)?;
401 let tip = resolve_diff_commit(repo, second)?;
402 let pathspecs = resolve_commit_diff_pathspecs(repo, cwd_prefix, &path_tokens)?;
403 Ok(trees(base, tip, pathspecs))
404 }
405 _ => Err(ResolveDiffArgumentsError::TooManyRevisions),
406 }
407}
408
409pub(crate) fn include_file_in_diff(file: &FileDiff, opts: &DiffOptions) -> bool {
410 if !opts.whitespace.is_active() || file.binary {
411 return true;
412 }
413 if matches!(
414 file.status,
415 DiffStatus::Renamed
416 | DiffStatus::Added
417 | DiffStatus::Deleted
418 | DiffStatus::Copied
419 | DiffStatus::TypeChanged
420 ) {
421 return true;
422 }
423 if file.old_mode != file.new_mode {
424 return true;
425 }
426 !file.hunks.is_empty()
427}
428
429pub(crate) fn collect_files_for_target(
430 repo: &Repository,
431 target: &DiffTarget,
432 pathspecs: &[String],
433 opts: DiffOptions,
434) -> Result<Vec<FileDiff>> {
435 let config = repo.config()?;
436 let thread_policy = repo.thread_policy();
437 let rename_threshold = opts.renames.threshold_percent().unwrap_or(101);
439
440 let files = match target {
441 DiffTarget::WorktreeVsHead => {
442 let head_tree = resolve_head_for_repository(repo)?.tree(repo.odb())?;
443 let changes = filter_file_changes(
444 worktree_changes(repo, head_tree.as_ref(), rename_threshold, pathspecs)?,
445 repo,
446 pathspecs,
447 )?;
448 file_diffs_from_worktree_changes(changes, &config, &thread_policy, opts.clone())?
449 }
450 DiffTarget::WorktreeVsTree(tree) => {
451 let changes = filter_file_changes(
452 worktree_changes(repo, Some(tree), rename_threshold, pathspecs)?,
453 repo,
454 pathspecs,
455 )?;
456 file_diffs_from_worktree_changes(changes, &config, &thread_policy, opts.clone())?
457 }
458 DiffTarget::IndexVsHead => {
459 let index = repo.index_handle().load()?;
460 let head_tree = resolve_head_for_repository(repo)?.tree(repo.odb())?;
461 let restrict =
462 pathspec_restrict_paths_for_scan(repo, &index, pathspecs, head_tree.as_ref())?;
463 let changes = filter_file_changes(
464 staged_changes(repo, rename_threshold, restrict)?,
465 repo,
466 pathspecs,
467 )?;
468 file_diffs_from_worktree_changes(changes, &config, &thread_policy, opts.clone())?
469 }
470 DiffTarget::WorktreeVsIndex => {
471 let changes = filter_file_changes(
472 unstaged_changes(repo, rename_threshold, pathspecs)?,
473 repo,
474 pathspecs,
475 )?;
476 file_diffs_from_worktree_changes(changes, &config, &thread_policy, opts.clone())?
477 }
478 DiffTarget::Commit(oid) => {
479 let items = commit_changes(
480 repo,
481 oid,
482 pathspecs,
483 &config,
484 &thread_policy,
485 opts.clone(),
486 rename_threshold,
487 )?;
488 filter_and_map_tree_structured(items, repo, pathspecs, opts)?
489 }
490 DiffTarget::Trees { old, new } => {
491 let items = if let Some(base) = old {
492 tree_commit_changes(
493 repo,
494 base,
495 new,
496 pathspecs,
497 &config,
498 &thread_policy,
499 opts.clone(),
500 rename_threshold,
501 )?
502 } else {
503 empty_tree_to_side_changes(
504 repo,
505 new,
506 pathspecs,
507 &config,
508 &thread_policy,
509 opts.clone(),
510 rename_threshold,
511 )?
512 };
513 filter_and_map_tree_structured(items, repo, pathspecs, opts)?
514 }
515 };
516 Ok(files)
517}
518
519pub(crate) fn stored_target_entries(
523 repo: &Repository,
524 target: &DiffTarget,
525 pathspecs: &[String],
526 options: &DiffOptions,
527) -> Result<Option<super::core::DiffEntriesResult>> {
528 let rename_threshold = options.rename_threshold_u32();
529 let entries = match target {
530 DiffTarget::Commit(oid) => {
531 let config = repo.config()?;
532 commit_diff_entries(
533 repo,
534 oid,
535 pathspecs,
536 &config,
537 &repo.thread_policy(),
538 rename_threshold,
539 )?
540 }
541 DiffTarget::Trees { old, new } => {
542 let config = repo.config()?;
543 let thread_policy = repo.thread_policy();
544 match old {
545 Some(base) => tree_commit_diff_entries(
546 repo,
547 base,
548 new,
549 pathspecs,
550 &config,
551 &thread_policy,
552 rename_threshold,
553 )?,
554 None => empty_tree_to_side_diff_entries(
555 repo,
556 new,
557 pathspecs,
558 &config,
559 &thread_policy,
560 rename_threshold,
561 )?,
562 }
563 }
564 DiffTarget::WorktreeVsHead
565 | DiffTarget::WorktreeVsTree(_)
566 | DiffTarget::IndexVsHead
567 | DiffTarget::WorktreeVsIndex => return Ok(None),
568 };
569 Ok(Some(entries))
570}
571
572pub(crate) fn limit_target_entries(
574 repo: &Repository,
575 mut result: super::core::DiffEntriesResult,
576 pathspecs: &[String],
577 options: &DiffOptions,
578) -> Result<super::core::DiffEntriesResult> {
579 let entries = if pathspecs.is_empty() {
580 result.entries
581 } else {
582 let matcher = repo.pathspec_matcher(pathspecs)?;
583 result
584 .entries
585 .into_iter()
586 .filter(|entry| diff_entry_matches(&matcher, entry))
587 .collect()
588 };
589 result.entries = apply_diff_filter(entries, options.filter);
590 Ok(result)
591}
592
593fn filter_and_map_tree_structured(
594 items: Vec<TreeDiffStructured>,
595 repo: &Repository,
596 pathspecs: &[String],
597 opts: DiffOptions,
598) -> Result<Vec<FileDiff>> {
599 Ok(filter_tree_diff(items, repo, pathspecs)?
600 .into_iter()
601 .map(file_diff_from_tree_structured)
602 .filter(|f| include_file_in_diff(f, &opts))
603 .collect())
604}
605
606fn file_diff_from_tree_structured(t: TreeDiffStructured) -> FileDiff {
607 let e = t.entry;
608 let path = e
609 .new_path
610 .clone()
611 .or_else(|| e.old_path.clone())
612 .unwrap_or_default();
613 let old_path = e.old_path.filter(|op| *op != path);
614 FileDiff {
615 path,
616 old_path,
617 status: e.status,
618 old_mode: t.structured.old_mode,
619 new_mode: t.structured.new_mode,
620 old_oid: e.old_oid,
621 new_oid: e.new_oid,
622 binary: t.binary,
623 old_size: t.old_size,
624 new_size: t.new_size,
625 content_changed: e.old_oid != e.new_oid,
626 old_encoding_lossy: t.structured.old_encoding_lossy,
627 new_encoding_lossy: t.structured.new_encoding_lossy,
628 hunks: t.structured.hunks,
629 line_stat: None,
630 }
631}
632
633fn file_diffs_from_worktree_changes(
634 changes: Vec<FileChange>,
635 config: &ConfigSet,
636 thread_policy: &ThreadPolicy,
637 opts: DiffOptions,
638) -> Result<Vec<FileDiff>> {
639 let opts = opts.cap_word_level_inline_for_changed_files(changes.len());
640 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
641 par_opts.min_items = 0;
642 let est_bytes = changes.len().saturating_mul(8192);
643 Ok(par_map(&changes, par_opts, est_bytes, |fc| {
644 build_file_diff(fc, opts.clone())
645 })
646 .into_iter()
647 .filter(|f| include_file_in_diff(f, &opts))
648 .collect())
649}
650
651fn build_file_diff(fc: &FileChange, diff_options: DiffOptions) -> FileDiff {
652 let structured = fc.structured(diff_options);
653 FileDiff {
654 path: fc.path.clone(),
655 old_path: fc.old_path.clone(),
656 status: fc.status,
657 old_mode: structured.old_mode,
658 new_mode: structured.new_mode,
659 old_oid: fc.old_oid,
660 new_oid: fc.new_oid,
661 binary: fc.binary,
662 old_size: fc.old_bytes.byte_len(),
663 new_size: fc.new_bytes.byte_len(),
664 content_changed: fc.content_changed,
665 old_encoding_lossy: structured.old_encoding_lossy,
666 new_encoding_lossy: structured.new_encoding_lossy,
667 hunks: structured.hunks,
668 line_stat: None,
669 }
670}
671
672fn token_is_diff_revision(repo: &Repository, token: &str) -> bool {
673 resolve_diff_commit(repo, token).is_ok()
674 || parse_commit_range(repo, token).ok().flatten().is_some()
675}
676
677fn token_is_known_worktree_pathspec(
678 repo: &Repository,
679 cwd_prefix: Option<&str>,
680 token: &str,
681) -> bool {
682 let Ok(resolved) = repo.resolve_worktree_pathspec(token, cwd_prefix) else {
683 return false;
684 };
685 pathspec_matches_known_in_repository(repo, &resolved).unwrap_or(false)
686}
687
688fn disambiguate_diff_revision_and_paths(
690 repo: &Repository,
691 cwd_prefix: Option<&str>,
692 mut commits: Vec<String>,
693 mut paths: Vec<String>,
694) -> (Vec<String>, Vec<String>) {
695 if !paths.is_empty() {
696 return (commits, paths);
697 }
698 let is_path = |token: &str| token_is_known_worktree_pathspec(repo, cwd_prefix, token);
699 match commits.as_slice() {
700 [first, second]
701 if token_is_diff_revision(repo, first)
702 && !token_is_diff_revision(repo, second)
703 && is_path(second) =>
704 {
705 paths.extend(commits.pop());
706 }
707 [only] if !token_is_diff_revision(repo, only) && is_path(only) => {
708 paths.extend(commits.pop());
709 }
710 _ => {}
711 }
712 if paths.is_empty() {
713 while let [_, .., tail] = commits.as_slice() {
714 if token_is_diff_revision(repo, tail) || !is_path(tail) {
715 break;
716 }
717 paths.extend(commits.pop());
718 }
719 }
720 (commits, paths)
721}
722
723fn pathspec_resolution_root(repo: &Repository) -> &Path {
724 repo.work_tree().unwrap_or_else(|| repo.git_dir())
725}
726
727fn resolve_worktree_diff_pathspecs(
729 repo: &Repository,
730 cwd_prefix: Option<&str>,
731 raw_paths: &[String],
732) -> ResolveResult<Vec<String>> {
733 if raw_paths.is_empty() {
734 return Ok(Vec::new());
735 }
736 if repo.work_tree().is_none() {
737 return Err(ResolveDiffArgumentsError::NoWorkTree);
738 }
739 raw_paths
740 .iter()
741 .map(|pathspec| {
742 let resolved = repo.resolve_worktree_pathspec(pathspec, cwd_prefix)?;
743 ensure_pathspec_matches_known_in_repository(repo, pathspec, &resolved)?;
744 Ok(resolved)
745 })
746 .collect()
747}
748
749pub fn resolve_commit_diff_pathspecs(
767 repo: &Repository,
768 cwd_prefix: Option<&str>,
769 raw_paths: &[String],
770) -> ResolveResult<Vec<String>> {
771 if repo.work_tree().is_none() {
772 let root = pathspec_resolution_root(repo);
773 return Ok(raw_paths
774 .iter()
775 .map(|pathspec| resolve_pathspec(pathspec, root, cwd_prefix))
776 .collect());
777 }
778 raw_paths
779 .iter()
780 .map(|pathspec| Ok(repo.resolve_worktree_pathspec(pathspec, cwd_prefix)?))
781 .collect()
782}
783
784fn filter_file_changes(
785 changes: Vec<FileChange>,
786 repo: &Repository,
787 specs: &[String],
788) -> Result<Vec<FileChange>> {
789 if specs.is_empty() {
790 return Ok(changes);
791 }
792 let matcher = repo.pathspec_matcher(specs)?;
793 Ok(changes
794 .into_iter()
795 .filter(|c| matcher.matches_change(&c.path, c.old_path.as_deref()))
796 .collect())
797}
798
799fn diff_entry_matches(matcher: &PathspecMatcher<'_>, entry: &DiffEntry) -> bool {
800 let path = entry
801 .new_path
802 .as_deref()
803 .or(entry.old_path.as_deref())
804 .unwrap_or("");
805 matcher.matches_change(path, entry.old_path.as_deref())
806}
807
808fn filter_tree_diff(
809 items: Vec<TreeDiffStructured>,
810 repo: &Repository,
811 specs: &[String],
812) -> Result<Vec<TreeDiffStructured>> {
813 if specs.is_empty() {
814 return Ok(items);
815 }
816 let matcher = repo.pathspec_matcher(specs)?;
817 Ok(items
818 .into_iter()
819 .filter(|item| diff_entry_matches(&matcher, &item.entry))
820 .collect())
821}
822
823fn unstaged_changes(
824 repo: &Repository,
825 rename_threshold: u32,
826 pathspecs: &[String],
827) -> Result<Vec<FileChange>> {
828 let mut index = repo.index_handle().load()?;
829 let restrict = pathspec_restrict_paths_for_scan(repo, &index, pathspecs, None)?;
830 unstaged_changes_inner(repo, &mut index, rename_threshold, restrict)
831}
832
833fn unstaged_changes_inner(
834 repo: &Repository,
835 index: &mut Index,
836 rename_threshold: u32,
837 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
838) -> Result<Vec<FileChange>> {
839 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
840 let rules = Arc::new(Mutex::new(WorktreeRules::from_repository(repo, index)?));
841 let entries = unstaged_entries_in(
842 repo,
843 index,
844 work_tree,
845 &rules,
846 rename_threshold,
847 restrict_to_paths,
848 )?;
849 let side = WorktreeSide {
850 work_tree,
851 index,
852 rules: &rules,
853 };
854 load_changes(repo.odb(), entries, Some(&side))
855}
856
857pub(super) fn diff_index_to_tree_for_paths(
858 repo: &Repository,
859 index: &Index,
860 head_tree: Option<&ObjectId>,
861 paths: &BTreeSet<String>,
862) -> Result<Vec<DiffEntry>> {
863 let odb = repo.odb();
864 let mut result = Vec::new();
865 for path in paths {
866 let index_entry = index.get(path.as_bytes(), 0);
867 if index_entry.is_some_and(|e| e.intent_to_add()) {
868 continue;
869 }
870 let tree_side = match head_tree {
871 Some(t) => odb
872 .tree_entry_at_path(t, path.as_bytes())?
873 .map(|e| (e.oid, e.mode)),
874 None => None,
875 };
876 let (idx_oid, idx_mode) = index_entry
877 .map(|e| (e.oid, e.mode))
878 .unwrap_or((ObjectId::zero(), 0));
879 let (tree_oid, tree_mode) = tree_side.unwrap_or((ObjectId::zero(), 0));
880 if index_entry.is_none() && tree_side.is_none() {
881 continue;
882 }
883 if index_entry.is_some() && tree_side.is_none() {
884 result.push(DiffEntry {
885 status: DiffStatus::Added,
886 old_path: None,
887 new_path: Some(path.clone()),
888 old_mode: FileMode::ABSENT,
889 new_mode: FileMode::new(idx_mode),
890 old_oid: ObjectId::zero(),
891 new_oid: idx_oid,
892 score: None,
893 });
894 continue;
895 }
896 if index_entry.is_none() {
897 result.push(DiffEntry {
898 status: DiffStatus::Deleted,
899 old_path: Some(path.clone()),
900 new_path: None,
901 old_mode: FileMode::new(tree_mode),
902 new_mode: FileMode::ABSENT,
903 old_oid: tree_oid,
904 new_oid: ObjectId::zero(),
905 score: None,
906 });
907 continue;
908 }
909 if idx_oid != tree_oid || idx_mode != tree_mode {
910 result.push(DiffEntry {
911 status: DiffStatus::for_content_change(tree_mode, idx_mode),
912 old_path: Some(path.clone()),
913 new_path: Some(path.clone()),
914 old_mode: FileMode::new(tree_mode),
915 new_mode: FileMode::new(idx_mode),
916 old_oid: tree_oid,
917 new_oid: idx_oid,
918 score: None,
919 });
920 }
921 }
922 Ok(result)
923}
924
925fn staged_changes(
926 repo: &Repository,
927 rename_threshold: u32,
928 restrict_to_paths: Option<Arc<BTreeSet<String>>>,
929) -> Result<Vec<FileChange>> {
930 let index = repo.index_handle().load()?;
931 let entries = staged_entries_in(repo, &index, rename_threshold, restrict_to_paths.as_deref())?;
932 load_object_changes(repo, &entries)
933}
934
935fn worktree_changes(
936 repo: &Repository,
937 base_tree: Option<&ObjectId>,
938 rename_threshold: u32,
939 pathspecs: &[String],
940) -> Result<Vec<FileChange>> {
941 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
942 let mut index = repo.index_handle().load()?;
943 let restrict = pathspec_restrict_paths_for_scan(repo, &index, pathspecs, base_tree)?;
944 let rules = Arc::new(Mutex::new(WorktreeRules::from_repository(repo, &index)?));
945 let entries = worktree_entries_in(
946 repo,
947 &mut index,
948 work_tree,
949 &rules,
950 base_tree,
951 rename_threshold,
952 restrict,
953 )?;
954 let side = WorktreeSide {
955 work_tree,
956 index: &index,
957 rules: &rules,
958 };
959 load_changes(repo.odb(), entries, Some(&side))
960}
961
962fn pathspec_matcher_for_stored_diff<'a>(
963 repo: &'a Repository,
964 pathspecs: &'a [String],
965) -> Result<Option<crate::pathspec::PathspecMatcher<'a>>> {
966 if pathspecs.is_empty() {
967 Ok(None)
968 } else {
969 Ok(Some(repo.pathspec_matcher(pathspecs)?))
970 }
971}
972
973fn tree_commit_diff_entries(
974 repo: &Repository,
975 base: &ObjectId,
976 tip: &ObjectId,
977 pathspecs: &[String],
978 config: &ConfigSet,
979 thread_policy: &ThreadPolicy,
980 rename_threshold: u32,
981) -> Result<super::core::DiffEntriesResult> {
982 let odb = repo.odb();
983 let base_tree = super::core::tree_oid_for_diff_side(odb, base)?;
984 let tip_tree = super::core::tree_oid_for_diff_side(odb, tip)?;
985 let pathspec_matcher = pathspec_matcher_for_stored_diff(repo, pathspecs)?;
986 let mut result = super::core::diff_trees_with_renames_for_pathspecs_and_policy_result(
987 odb,
988 Some(&base_tree),
989 Some(&tip_tree),
990 "",
991 pathspecs,
992 pathspec_matcher.as_ref(),
993 rename_threshold,
994 config,
995 thread_policy,
996 )?;
997 sort_entries_by_path(&mut result.entries);
998 Ok(result)
999}
1000
1001#[allow(clippy::too_many_arguments)]
1002fn tree_commit_changes(
1003 repo: &Repository,
1004 base: &ObjectId,
1005 tip: &ObjectId,
1006 pathspecs: &[String],
1007 config: &ConfigSet,
1008 thread_policy: &ThreadPolicy,
1009 diff_options: DiffOptions,
1010 rename_threshold: u32,
1011) -> Result<Vec<TreeDiffStructured>> {
1012 let result = tree_commit_diff_entries(
1013 repo,
1014 base,
1015 tip,
1016 pathspecs,
1017 config,
1018 thread_policy,
1019 rename_threshold,
1020 )?;
1021 tree_diff_structured_parallel(
1022 repo.odb(),
1023 result.entries,
1024 config,
1025 thread_policy,
1026 diff_options,
1027 )
1028}
1029
1030fn commit_diff_entries(
1031 repo: &Repository,
1032 oid: &ObjectId,
1033 pathspecs: &[String],
1034 config: &ConfigSet,
1035 thread_policy: &ThreadPolicy,
1036 rename_threshold: u32,
1037) -> Result<super::core::DiffEntriesResult> {
1038 let commit = repo.read_commit_with_replace(oid)?;
1039 let new_tree = commit.tree;
1040 let parent_ctx = crate::shallow::EffectiveCommitParents::load(repo);
1041 let effective_parents = parent_ctx.for_commit(*oid, &commit.parents);
1042 let old_tree = match effective_parents.first() {
1043 Some(parent) => Some(repo.read_commit_with_replace(parent)?.tree),
1044 None => None,
1045 };
1046
1047 let pathspec_matcher = pathspec_matcher_for_stored_diff(repo, pathspecs)?;
1048 let mut result = super::core::diff_trees_with_renames_for_pathspecs_and_policy_result(
1049 repo.odb(),
1050 old_tree.as_ref(),
1051 Some(&new_tree),
1052 "",
1053 pathspecs,
1054 pathspec_matcher.as_ref(),
1055 rename_threshold,
1056 config,
1057 thread_policy,
1058 )?;
1059 sort_entries_by_path(&mut result.entries);
1060 Ok(result)
1061}
1062
1063#[allow(clippy::too_many_arguments)]
1064fn commit_changes(
1065 repo: &Repository,
1066 oid: &ObjectId,
1067 pathspecs: &[String],
1068 config: &ConfigSet,
1069 thread_policy: &ThreadPolicy,
1070 diff_options: DiffOptions,
1071 rename_threshold: u32,
1072) -> Result<Vec<TreeDiffStructured>> {
1073 let result = commit_diff_entries(
1074 repo,
1075 oid,
1076 pathspecs,
1077 config,
1078 thread_policy,
1079 rename_threshold,
1080 )?;
1081 tree_diff_structured_parallel(
1082 repo.odb(),
1083 result.entries,
1084 config,
1085 thread_policy,
1086 diff_options,
1087 )
1088}
1089
1090fn empty_tree_to_side_diff_entries(
1091 repo: &Repository,
1092 side: &ObjectId,
1093 pathspecs: &[String],
1094 config: &ConfigSet,
1095 thread_policy: &ThreadPolicy,
1096 rename_threshold: u32,
1097) -> Result<super::core::DiffEntriesResult> {
1098 let tip_tree = super::core::tree_oid_for_diff_side(repo.odb(), side)?;
1099 let pathspec_matcher = pathspec_matcher_for_stored_diff(repo, pathspecs)?;
1100 let mut result = super::core::diff_trees_with_renames_for_pathspecs_and_policy_result(
1101 repo.odb(),
1102 None,
1103 Some(&tip_tree),
1104 "",
1105 pathspecs,
1106 pathspec_matcher.as_ref(),
1107 rename_threshold,
1108 config,
1109 thread_policy,
1110 )?;
1111 sort_entries_by_path(&mut result.entries);
1112 Ok(result)
1113}
1114
1115#[allow(clippy::too_many_arguments)]
1116fn empty_tree_to_side_changes(
1117 repo: &Repository,
1118 side: &ObjectId,
1119 pathspecs: &[String],
1120 config: &ConfigSet,
1121 thread_policy: &ThreadPolicy,
1122 diff_options: DiffOptions,
1123 rename_threshold: u32,
1124) -> Result<Vec<TreeDiffStructured>> {
1125 let result = empty_tree_to_side_diff_entries(
1126 repo,
1127 side,
1128 pathspecs,
1129 config,
1130 thread_policy,
1131 rename_threshold,
1132 )?;
1133 tree_diff_structured_parallel(
1134 repo.odb(),
1135 result.entries,
1136 config,
1137 thread_policy,
1138 diff_options,
1139 )
1140}
1141
1142fn peel_diff_commit(repo: &Repository, oid: ObjectId) -> ResolveResult<ObjectId> {
1143 peel_tags_to_commit(repo, oid)?.map_err(|oid| ResolveDiffArgumentsError::NotACommit { oid })
1144}
1145
1146fn resolve_diff_commit(repo: &Repository, spec: &str) -> ResolveResult<ObjectId> {
1148 let oid = resolve_revision(repo, spec).map_err(|source| {
1149 ResolveDiffArgumentsError::UnresolvedRevision {
1150 spec: spec.to_owned(),
1151 source: Box::new(source),
1152 }
1153 })?;
1154 peel_diff_commit(repo, oid)
1155}
1156
1157fn resolve_diff_range_end(repo: &Repository, spec: &str) -> ResolveResult<ObjectId> {
1159 let spec = if spec.is_empty() { "HEAD" } else { spec };
1160 peel_diff_commit(repo, resolve_revision_for_range_end(repo, spec)?)
1161}
1162
1163fn parse_commit_range(
1165 repo: &Repository,
1166 spec: &str,
1167) -> ResolveResult<Option<(ObjectId, ObjectId)>> {
1168 if let Some((left, right)) = split_triple_dot_range(spec) {
1169 let left = resolve_diff_range_end(repo, left)?;
1170 let right = resolve_diff_range_end(repo, right)?;
1171 let base = merge_base_for_diff_two_commits(repo, left, right).map_err(|e| match e {
1172 MergeBaseForDiffError::None => ResolveDiffArgumentsError::NoMergeBase,
1173 MergeBaseForDiffError::Multiple => ResolveDiffArgumentsError::MultipleMergeBases,
1174 MergeBaseForDiffError::Other(msg) => Error::Message(msg).into(),
1175 })?;
1176 return Ok(Some((base, right)));
1177 }
1178 let Some((left, right)) = split_double_dot_range(spec) else {
1179 return Ok(None);
1180 };
1181 Ok(Some((
1182 resolve_diff_range_end(repo, left)?,
1183 resolve_diff_range_end(repo, right)?,
1184 )))
1185}
1186
1187pub(crate) type UnstagedDiffWithPeers = (
1189 Vec<FileDiff>,
1190 std::collections::HashMap<String, Vec<StructuredHunk>>,
1191);
1192
1193pub(crate) type UnstagedRawWithPeers = (
1194 Vec<FileChange>,
1195 std::collections::HashMap<String, Vec<StructuredHunk>>,
1196);
1197
1198pub(crate) fn unstaged_raw_changes_with_peer_hunks(
1200 repo: &Repository,
1201 pathspecs: &[String],
1202 opts: DiffOptions,
1203) -> Result<UnstagedRawWithPeers> {
1204 let mut index = repo.index_handle().load()?;
1205 unstaged_raw_changes_with_peer_hunks_in(repo, &mut index, pathspecs, opts)
1206}
1207
1208pub(crate) fn unstaged_raw_changes_with_peer_hunks_in(
1212 repo: &Repository,
1213 index: &mut Index,
1214 pathspecs: &[String],
1215 opts: DiffOptions,
1216) -> Result<UnstagedRawWithPeers> {
1217 let rename_threshold = opts.renames.threshold_percent().unwrap_or(101);
1218 let literal_paths = exact_literal_pathspec_set(pathspecs, PathspecMatchContext::default());
1219 let head_tree = match literal_paths {
1220 Some(_) => resolve_head_for_repository(repo)?.tree(repo.odb())?,
1221 None => None,
1222 };
1223 let peer_hunks = staged_peer_hunks_for_pathspecs(
1224 repo,
1225 index,
1226 head_tree,
1227 literal_paths.as_ref(),
1228 pathspecs,
1229 opts.clone(),
1230 rename_threshold,
1231 )?;
1232 let restrict = literal_paths.filter(|paths| {
1233 paths
1234 .iter()
1235 .all(|p| path_is_exact_tracked_file(repo, index, head_tree, p).unwrap_or(false))
1236 });
1237 let unstaged = if let Some(paths) = restrict {
1238 unstaged_changes_inner(repo, index, rename_threshold, Some(Arc::new(paths)))?
1239 } else {
1240 filter_file_changes(
1241 unstaged_changes_inner(repo, index, rename_threshold, None)?,
1242 repo,
1243 pathspecs,
1244 )?
1245 };
1246 Ok((unstaged, peer_hunks))
1247}
1248
1249fn path_has_staged_delta_vs_head_with_index(
1250 repo: &Repository,
1251 index: &Index,
1252 head_tree: Option<ObjectId>,
1253 path: &str,
1254) -> Result<bool> {
1255 let path_bytes = path.as_bytes();
1256 if index.get(path_bytes, 2).is_some() || index.get(path_bytes, 3).is_some() {
1257 return Ok(true);
1258 }
1259 let head_blob = match head_tree {
1260 Some(tree) => repo
1261 .odb()
1262 .tree_entry_at_path(&tree, path_bytes)?
1263 .map(|e| e.oid),
1264 None => None,
1265 };
1266 let Some(idx_entry) = index.get(path_bytes, 0) else {
1267 return Ok(head_blob.is_some());
1268 };
1269 match head_blob {
1270 None => Ok(true),
1271 Some(h) => Ok(idx_entry.oid != h),
1272 }
1273}
1274
1275fn path_is_exact_tracked_file(
1276 repo: &Repository,
1277 index: &Index,
1278 head_tree: Option<ObjectId>,
1279 path: &str,
1280) -> Result<bool> {
1281 let path_bytes = path.as_bytes();
1282 if let Some(entry) = index.get(path_bytes, 0) {
1283 return Ok(entry.mode != MODE_TREE);
1284 }
1285 match head_tree {
1286 Some(tree) => Ok(repo.odb().tree_entry_at_path(&tree, path_bytes)?.is_some()),
1287 None => Ok(false),
1288 }
1289}
1290
1291fn staged_peer_hunks_for_pathspecs(
1294 repo: &Repository,
1295 index: &Index,
1296 head_tree: Option<ObjectId>,
1297 literal_paths: Option<&BTreeSet<String>>,
1298 pathspecs: &[String],
1299 opts: DiffOptions,
1300 rename_threshold: u32,
1301) -> Result<std::collections::HashMap<String, Vec<StructuredHunk>>> {
1302 if let Some(literal_paths) = literal_paths {
1303 let all_exact_files = literal_paths
1304 .iter()
1305 .all(|p| path_is_exact_tracked_file(repo, index, head_tree, p).unwrap_or(false));
1306 if all_exact_files
1307 && literal_paths.iter().all(|p| {
1308 path_has_staged_delta_vs_head_with_index(repo, index, head_tree, p)
1309 .map(|delta| !delta)
1310 .unwrap_or(false)
1311 })
1312 {
1313 return Ok(std::collections::HashMap::new());
1314 }
1315 }
1316 let staged = filter_file_changes(
1317 staged_changes(repo, rename_threshold, None)?,
1318 repo,
1319 pathspecs,
1320 )?;
1321 Ok(staged
1322 .iter()
1323 .map(|fc| {
1324 let structured = fc.structured(opts.clone());
1325 (fc.path.clone(), structured.hunks)
1326 })
1327 .collect())
1328}
1329
1330pub(crate) fn unstaged_file_diffs_with_peer_hunks(
1332 repo: &Repository,
1333 pathspecs: &[String],
1334 opts: DiffOptions,
1335) -> Result<UnstagedDiffWithPeers> {
1336 let config = repo.config()?;
1337 let thread_policy = repo.thread_policy();
1338 let rename_threshold = opts.renames.threshold_percent().unwrap_or(101);
1339 let staged = filter_file_changes(
1340 staged_changes(repo, rename_threshold, None)?,
1341 repo,
1342 pathspecs,
1343 )?;
1344 let peer_hunks: std::collections::HashMap<String, Vec<StructuredHunk>> = staged
1345 .iter()
1346 .map(|fc| {
1347 let structured = fc.structured(opts.clone());
1348 (fc.path.clone(), structured.hunks)
1349 })
1350 .collect();
1351 let unstaged = filter_file_changes(
1352 unstaged_changes(repo, rename_threshold, pathspecs)?,
1353 repo,
1354 pathspecs,
1355 )?;
1356 let files = file_diffs_from_worktree_changes(unstaged, &config, &thread_policy, opts)?;
1357 Ok((files, peer_hunks))
1358}
1359
1360pub struct HunkDiffBundle {
1362 pub unstaged: Vec<FileDiff>,
1364 pub peer_hunks: std::collections::HashMap<String, Vec<StructuredHunk>>,
1366}
1367
1368pub fn hunk_diff_bundle(
1377 repo: &Repository,
1378 pathspecs: &[String],
1379 opts: DiffOptions,
1380) -> Result<HunkDiffBundle> {
1381 let config = repo.config()?;
1382 let thread_policy = repo.thread_policy();
1383 let (unstaged_fc, peer_hunks) =
1384 unstaged_raw_changes_with_peer_hunks(repo, pathspecs, opts.clone())?;
1385 let unstaged = file_diffs_from_worktree_changes(unstaged_fc, &config, &thread_policy, opts)?;
1386 Ok(HunkDiffBundle {
1387 unstaged,
1388 peer_hunks,
1389 })
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394 use super::*;
1395
1396 #[test]
1397 fn include_file_omits_whitespace_only_modified() {
1398 let file = FileDiff {
1399 path: "x.txt".into(),
1400 old_path: None,
1401 status: DiffStatus::Modified,
1402 old_mode: Some("100644".into()),
1403 new_mode: Some("100644".into()),
1404 old_oid: ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap(),
1405 new_oid: ObjectId::from_hex("0000000000000000000000000000000000000002").unwrap(),
1406 binary: false,
1407 old_size: 0,
1408 new_size: 0,
1409 content_changed: true,
1410 old_encoding_lossy: false,
1411 new_encoding_lossy: false,
1412 hunks: Vec::new(),
1413 line_stat: None,
1414 };
1415 let opts =
1416 DiffOptions::default().with_whitespace(crate::diff::WhitespaceMode::ignore_all_space());
1417 assert!(!include_file_in_diff(&file, &opts));
1418 }
1419
1420 #[test]
1421 fn include_file_omits_ws_only_even_when_content_changed_false() {
1422 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
1423 let file = FileDiff {
1424 path: "x.txt".into(),
1425 old_path: None,
1426 status: DiffStatus::Modified,
1427 old_mode: Some("100644".into()),
1428 new_mode: Some("100644".into()),
1429 old_oid: oid,
1430 new_oid: oid,
1431 binary: false,
1432 old_size: 0,
1433 new_size: 0,
1434 content_changed: false,
1435 old_encoding_lossy: false,
1436 new_encoding_lossy: false,
1437 hunks: Vec::new(),
1438 line_stat: None,
1439 };
1440 let opts =
1441 DiffOptions::default().with_whitespace(crate::diff::WhitespaceMode::ignore_all_space());
1442 assert!(!include_file_in_diff(&file, &opts));
1443 }
1444
1445 #[test]
1446 fn include_file_keeps_mode_only_under_whitespace() {
1447 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
1448 let file = FileDiff {
1449 path: "x.txt".into(),
1450 old_path: None,
1451 status: DiffStatus::Modified,
1452 old_mode: Some("100644".into()),
1453 new_mode: Some("100755".into()),
1454 old_oid: oid,
1455 new_oid: oid,
1456 binary: false,
1457 old_size: 0,
1458 new_size: 0,
1459 content_changed: false,
1460 old_encoding_lossy: false,
1461 new_encoding_lossy: false,
1462 hunks: Vec::new(),
1463 line_stat: None,
1464 };
1465 let opts =
1466 DiffOptions::default().with_whitespace(crate::diff::WhitespaceMode::ignore_all_space());
1467 assert!(include_file_in_diff(&file, &opts));
1468 }
1469
1470 #[test]
1471 fn include_file_keeps_whitespace_only_rename() {
1472 let file = FileDiff {
1473 path: "new.txt".into(),
1474 old_path: Some("old.txt".into()),
1475 status: DiffStatus::Renamed,
1476 old_mode: Some("100644".into()),
1477 new_mode: Some("100644".into()),
1478 old_oid: ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap(),
1479 new_oid: ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap(),
1480 binary: false,
1481 old_size: 0,
1482 new_size: 0,
1483 content_changed: false,
1484 old_encoding_lossy: false,
1485 new_encoding_lossy: false,
1486 hunks: Vec::new(),
1487 line_stat: None,
1488 };
1489 let opts =
1490 DiffOptions::default().with_whitespace(crate::diff::WhitespaceMode::ignore_all_space());
1491 assert!(include_file_in_diff(&file, &opts));
1492 }
1493
1494 #[test]
1495 fn worktree_targets_have_no_stored_entries() {
1496 let dir = tempfile::tempdir().expect("tempdir");
1497 let repo =
1498 Repository::init(dir.path(), &crate::repo::InitOptions::default()).expect("init");
1499 for target in [
1500 DiffTarget::WorktreeVsHead,
1501 DiffTarget::WorktreeVsTree(ObjectId::zero()),
1502 DiffTarget::IndexVsHead,
1503 DiffTarget::WorktreeVsIndex,
1504 ] {
1505 let entries = stored_target_entries(&repo, &target, &[], &DiffOptions::default())
1506 .expect("no object reads");
1507 assert_eq!(entries, None, "{target:?}");
1508 }
1509 }
1510
1511 #[test]
1512 fn limit_target_entries_applies_pathspecs_then_diff_filter() {
1513 let dir = tempfile::tempdir().expect("tempdir");
1514 let repo =
1515 Repository::init(dir.path(), &crate::repo::InitOptions::default()).expect("init");
1516 let entry = |status: DiffStatus, path: &str| DiffEntry {
1517 status,
1518 old_path: (status != DiffStatus::Added).then(|| path.to_owned()),
1519 new_path: (status != DiffStatus::Deleted).then(|| path.to_owned()),
1520 old_mode: FileMode::REGULAR,
1521 new_mode: FileMode::REGULAR,
1522 old_oid: ObjectId::zero(),
1523 new_oid: ObjectId::zero(),
1524 score: None,
1525 };
1526 let entries = crate::diff::DiffEntriesResult {
1527 entries: vec![
1528 entry(DiffStatus::Added, "a.txt"),
1529 entry(DiffStatus::Deleted, "dir/b.txt"),
1530 entry(DiffStatus::Modified, "dir/c.rs"),
1531 ],
1532 inexact_rename_skipped: None,
1533 };
1534 let paths = |kept: Vec<DiffEntry>| -> Vec<String> {
1535 kept.iter()
1536 .map(|e| {
1537 e.new_path
1538 .clone()
1539 .or(e.old_path.clone())
1540 .unwrap_or_default()
1541 })
1542 .collect()
1543 };
1544 let all = DiffOptions::default();
1545 let kept = limit_target_entries(&repo, entries.clone(), &[], &all).expect("limit");
1546 assert_eq!(paths(kept.entries), ["a.txt", "dir/b.txt", "dir/c.rs"]);
1547
1548 let kept =
1549 limit_target_entries(&repo, entries.clone(), &["dir".to_owned()], &all).expect("dir");
1550 assert_eq!(paths(kept.entries), ["dir/b.txt", "dir/c.rs"]);
1551
1552 let added_or_deleted = all.with_filter(Some("AD".parse().expect("filter")));
1553 let kept =
1554 limit_target_entries(&repo, entries.clone(), &[], &added_or_deleted).expect("filter");
1555 assert_eq!(paths(kept.entries), ["a.txt", "dir/b.txt"]);
1556
1557 let kept = limit_target_entries(&repo, entries, &["dir".to_owned()], &added_or_deleted)
1558 .expect("both");
1559 assert_eq!(paths(kept.entries), ["dir/b.txt"]);
1560 }
1561
1562 #[test]
1563 fn failed_revision_with_file_paths_keeps_revision_error() {
1564 let dir = tempfile::tempdir().expect("tempdir");
1565 let repo =
1566 Repository::init(dir.path(), &crate::repo::InitOptions::default()).expect("init");
1567 let request = DiffRequest {
1568 positionals: vec!["unknown-revision".into()],
1569 pathspec_file: Some(vec!["selected.txt".into()]),
1570 ..DiffRequest::default()
1571 };
1572 let err = resolve_diff_request(&repo, None, &request).expect_err("unknown revision");
1573 assert!(
1574 matches!(err, ResolveDiffArgumentsError::UnresolvedRevision { spec, .. } if spec == "unknown-revision")
1575 );
1576 }
1577
1578 #[test]
1579 fn resolve_errors_staged_and_unstaged() {
1580 let dir = tempfile::tempdir().expect("tempdir");
1581 let repo = crate::repo::Repository::init(dir.path(), &crate::repo::InitOptions::default())
1582 .expect("init");
1583 let err = resolve_diff_arguments(&repo, None, true, true, None, &[], &[]).unwrap_err();
1584 assert!(matches!(err, ResolveDiffArgumentsError::StagedAndUnstaged));
1585 }
1586}