Expand 7 hidden lines 8 8 9 9 10 10 11 + use std:: collections:: HashMap ; 11 12 use std:: path:: Path ; 12 13 13 - use git2:: { BranchType , Commit , Repository , Signature , Sort , TreeEntry as GitTreeEntry } ; 14 + use git2:: { 15 + BranchType , Commit , Delta , DiffFindOptions , DiffLineType , DiffOptions , Patch , Repository , 16 + Signature , Sort , Tree , TreeEntry as GitTreeEntry , 17 + } ; 14 18 15 19 use crate :: GitError ; 16 20 use crate :: types:: { 17 - Blob , BranchInfo , CommitDetail , CommitSummary , EntryKind , Oid , Page , Person , RefKind , Resolved , 18 - TagInfo , TreeEntry , 21 + Blob , BranchInfo , CommitDetail , CommitSummary , DeltaStatus , DiffLine , DiffOpts , EntryKind , 22 + FileDiff , FileDiffs , Hunk , LineOrigin , Oid , Page , Person , RefKind , Resolved , TagInfo , 23 + TreeEntry , 19 24 } ; 20 25 21 26 Expand 273 hidden lines 295 300 } ) 296 301 } 297 302 303 + 304 + 305 + 306 + 307 + 308 + 309 + 310 + 311 + 312 + 313 + pub fn diff ( 314 + & self , 315 + base : Option < & Oid > , 316 + head : & Oid , 317 + opts : DiffOpts , 318 + ) -> Result < FileDiffs , GitError > { 319 + let head_tree = self . commit_at ( head) ?. tree ( ) ?; 320 + let base_tree = match base { 321 + Some ( oid) => Some ( self . commit_at ( oid) ?. tree ( ) ?) , 322 + None => None , 323 + } ; 324 + 325 + let mut diff_opts = DiffOptions :: new ( ) ; 326 + diff_opts. context_lines ( opts. context_lines) ; 327 + let mut diff = self . inner. diff_tree_to_tree ( 328 + base_tree. as_ref ( ) , 329 + Some ( & head_tree) , 330 + Some ( & mut diff_opts) , 331 + ) ?; 332 + diff. find_similar ( Some ( DiffFindOptions :: new ( ) . renames ( true ) ) ) ?; 333 + 334 + let mut files = Vec :: new ( ) ; 335 + for idx in 0 ..diff. deltas ( ) . len ( ) { 336 + let Some ( delta) = diff. get_delta ( idx) else { 337 + continue ; 338 + } ; 339 + let patch = Patch :: from_diff ( & diff, idx) ?; 340 + let is_binary = patch. is_none ( ) || delta. flags ( ) . is_binary ( ) ; 341 + let hunks = match & patch { 342 + Some ( patch) if !is_binary => extract_hunks ( patch) ?, 343 + _ => Vec :: new ( ) , 344 + } ; 345 + files. push ( FileDiff { 346 + old_path: delta. old_file ( ) . path ( ) . map ( path_string) , 347 + new_path: delta. new_file ( ) . path ( ) . map ( path_string) , 348 + status: map_status ( delta. status ( ) ) , 349 + is_binary, 350 + hunks, 351 + } ) ; 352 + } 353 + Ok ( FileDiffs { files } ) 354 + } 355 + 356 + 357 + 358 + 359 + 360 + 361 + 362 + 363 + 364 + pub fn diff_context ( 365 + & self , 366 + head : & Oid , 367 + file : & Path , 368 + from : u32 , 369 + to : u32 , 370 + ) -> Result < Vec < DiffLine > , GitError > { 371 + let commit = self . commit_at ( head) ?; 372 + let entry = commit 373 + . tree ( ) ? 374 + . get_path ( file) 375 + . map_err ( |_| GitError :: PathNotFound ( file. display ( ) . to_string ( ) ) ) ?; 376 + let blob = entry 377 + . to_object ( & self . inner) ? 378 + . into_blob ( ) 379 + . map_err ( |_| GitError :: NotAFile ( file. display ( ) . to_string ( ) ) ) ?; 380 + let text = String :: from_utf8_lossy ( blob. content ( ) ) ; 381 + let lines: Vec < & str > = text. split_inclusive ( '\n' ) . collect ( ) ; 382 + 383 + let start = from. saturating_sub ( 1 ) as usize ; 384 + let end = ( to as usize ) . min ( lines. len ( ) ) ; 385 + let mut out = Vec :: new ( ) ; 386 + for ( offset, line) in lines. get ( start..end) . unwrap_or ( & [ ] ) . iter ( ) . enumerate ( ) { 387 + let lineno = from + u32:: try_from ( offset) . unwrap_or ( 0 ) ; 388 + out. push ( DiffLine { 389 + origin: LineOrigin :: Context , 390 + old_lineno: None , 391 + new_lineno: Some ( lineno) , 392 + content: ( * line) . to_string ( ) , 393 + } ) ; 394 + } 395 + Ok ( out) 396 + } 397 + 398 + 399 + 400 + 401 + 402 + 403 + 404 + 405 + 406 + 407 + 408 + pub fn last_commit_per_entry ( 409 + & self , 410 + rev : & Resolved , 411 + path : & Path , 412 + limit : usize , 413 + ) -> Result < HashMap < String , CommitSummary > , GitError > { 414 + let mut remaining: std:: collections:: HashSet < String > = self 415 + . tree_entries ( rev, path) ? 416 + . into_iter ( ) 417 + . map ( |entry| entry. name) 418 + . collect ( ) ; 419 + let mut result = HashMap :: new ( ) ; 420 + 421 + let start = git2:: Oid :: from_str ( rev. oid. as_str ( ) ) ?; 422 + let mut walk = self . inner. revwalk ( ) ?; 423 + walk. set_sorting ( Sort :: TIME ) ?; 424 + walk. push ( start) ?; 425 + 426 + for ( count, oid) in walk. enumerate ( ) { 427 + if remaining. is_empty ( ) || count >= limit { 428 + break ; 429 + } 430 + let commit = self . inner. find_commit ( oid?) ?; 431 + let here = self . subtree_at ( & commit, path) ?; 432 + let parent_tree = commit 433 + . parent ( 0 ) 434 + . ok ( ) 435 + . map ( |parent| self . subtree_at ( & parent, path) ) 436 + . transpose ( ) ? 437 + . flatten ( ) ; 438 + 439 + let mut resolved = Vec :: new ( ) ; 440 + for name in & remaining { 441 + let here_oid = here. as_ref ( ) . and_then ( |t| t. get_name ( name) ) . map ( |e| e. id ( ) ) ; 442 + let parent_oid = parent_tree 443 + . as_ref ( ) 444 + . and_then ( |t| t. get_name ( name) ) 445 + . map ( |e| e. id ( ) ) ; 446 + 447 + if here_oid != parent_oid && here_oid. is_some ( ) { 448 + resolved. push ( name. clone ( ) ) ; 449 + } 450 + } 451 + for name in resolved { 452 + result. insert ( name. clone ( ) , commit_summary ( & commit) ) ; 453 + remaining. remove ( & name) ; 454 + } 455 + } 456 + Ok ( result) 457 + } 458 + 459 + 460 + 461 + fn subtree_at < ' a > ( 462 + & ' a self , 463 + commit : & Commit < ' a > , 464 + path : & Path , 465 + ) -> Result < Option < Tree < ' a > > , GitError > { 466 + let root = commit. tree ( ) ?; 467 + if path. components ( ) . next ( ) . is_none ( ) { 468 + return Ok ( Some ( root) ) ; 469 + } 470 + match root. get_path ( path) { 471 + Ok ( entry) => Ok ( entry. to_object ( & self . inner) ?. into_tree ( ) . ok ( ) ) , 472 + Err ( _) => Ok ( None ) , 473 + } 474 + } 475 + 298 476 299 477 fn commit_at ( & self , oid : & Oid ) -> Result < Commit < ' _ > , GitError > { 300 478 let parsed = git2:: Oid :: from_str ( oid. as_str ( ) ) Expand 55 hidden lines 356 534 Oid :: new ( oid. to_string ( ) ) 357 535 } 358 536 537 + 538 + fn path_string ( path : & Path ) -> String { 539 + path. display ( ) . to_string ( ) 540 + } 541 + 542 + 543 + 544 + fn map_status ( status : Delta ) -> DeltaStatus { 545 + match status { 546 + Delta :: Added => DeltaStatus :: Added , 547 + Delta :: Deleted => DeltaStatus :: Deleted , 548 + Delta :: Renamed | Delta :: Copied => DeltaStatus :: Renamed , 549 + Delta :: Typechange => DeltaStatus :: TypeChange , 550 + _ => DeltaStatus :: Modified , 551 + } 552 + } 553 + 554 + 555 + fn extract_hunks ( patch : & Patch < ' _ > ) -> Result < Vec < Hunk > , GitError > { 556 + let mut hunks = Vec :: with_capacity ( patch. num_hunks ( ) ) ; 557 + for h in 0 ..patch. num_hunks ( ) { 558 + let ( hunk, line_count) = patch. hunk ( h) ?; 559 + let header = String :: from_utf8_lossy ( hunk. header ( ) ) 560 + . trim_end_matches ( [ '\r' , '\n' ] ) 561 + . to_string ( ) ; 562 + let mut lines = Vec :: with_capacity ( line_count) ; 563 + for l in 0 ..line_count { 564 + let line = patch. line_in_hunk ( h, l) ?; 565 + let origin = match line. origin_value ( ) { 566 + DiffLineType :: Addition | DiffLineType :: AddEOFNL => LineOrigin :: Addition , 567 + DiffLineType :: Deletion | DiffLineType :: DeleteEOFNL => LineOrigin :: Deletion , 568 + _ => LineOrigin :: Context , 569 + } ; 570 + lines. push ( DiffLine { 571 + origin, 572 + old_lineno: line. old_lineno ( ) , 573 + new_lineno: line. new_lineno ( ) , 574 + content: String :: from_utf8_lossy ( line. content ( ) ) . to_string ( ) , 575 + } ) ; 576 + } 577 + hunks. push ( Hunk { header, lines } ) ; 578 + } 579 + Ok ( hunks) 580 + } 581 + 359 582 360 583 361 584 fn person ( sig : & Signature < ' _ > ) -> Person { Expand 326 hidden lines 688 911 } 689 912 690 913 # [ test ] 914 + fn diff_reports_added_and_modified_files_with_hunks ( ) { 915 + let fx = fixture ( ) ; 916 + let repo = open ( & fx) ; 917 + let c1 = Oid :: new ( fx. commits[ 0 ] . to_string ( ) ) ; 918 + let c2 = Oid :: new ( fx. commits[ 1 ] . to_string ( ) ) ; 919 + 920 + 921 + let initial = repo. diff ( None , & c1, DiffOpts :: default ( ) ) . unwrap ( ) ; 922 + let paths: Vec < _ > = initial 923 + . files 924 + . iter ( ) 925 + . filter_map ( |f| f. new_path. as_deref ( ) ) 926 + . collect ( ) ; 927 + assert ! ( paths. contains ( & "README.md" ) ) ; 928 + assert ! ( paths. contains ( & "src/main.rs" ) ) ; 929 + assert ! ( initial. files. iter ( ) . all ( |f| f. status == DeltaStatus :: Added ) ) ; 930 + 931 + 932 + let delta = repo. diff ( Some ( & c1) , & c2, DiffOpts :: default ( ) ) . unwrap ( ) ; 933 + assert_eq ! ( delta. files. len ( ) , 1 ) ; 934 + let readme = & delta. files[ 0 ] ; 935 + assert_eq ! ( readme. new_path. as_deref ( ) , Some ( "README.md" ) ) ; 936 + assert_eq ! ( readme. status, DeltaStatus :: Modified ) ; 937 + assert ! ( !readme. is_binary) ; 938 + assert ! ( !readme. hunks. is_empty ( ) ) ; 939 + 940 + let has_addition = readme 941 + . hunks 942 + . iter ( ) 943 + . flat_map ( |h| & h. lines) 944 + . any ( |l| l. origin == LineOrigin :: Addition && l. content. contains ( "hello world" ) ) ; 945 + assert ! ( has_addition, "expected the new line in the hunk" ) ; 946 + } 947 + 948 + # [ test ] 949 + fn diff_context_returns_post_image_lines ( ) { 950 + let fx = fixture ( ) ; 951 + let repo = open ( & fx) ; 952 + let c2 = Oid :: new ( fx. commits[ 1 ] . to_string ( ) ) ; 953 + 954 + 955 + let lines = repo 956 + . diff_context ( & c2, Path :: new ( "README.md" ) , 1 , 5 ) 957 + . unwrap ( ) ; 958 + assert_eq ! ( lines. len ( ) , 1 , "clamped to the file length" ) ; 959 + assert_eq ! ( lines[ 0 ] . origin, LineOrigin :: Context ) ; 960 + assert_eq ! ( lines[ 0 ] . new_lineno, Some ( 1 ) ) ; 961 + assert_eq ! ( lines[ 0 ] . content, "hello world \n " ) ; 962 + } 963 + 964 + # [ test ] 965 + fn last_commit_per_entry_attributes_the_right_change ( ) { 966 + let fx = fixture ( ) ; 967 + let repo = open ( & fx) ; 968 + let main = repo. resolve_ref ( "main" ) . unwrap ( ) ; 969 + 970 + let latest = repo 971 + . last_commit_per_entry ( & main, Path :: new ( "" ) , 100 ) 972 + . unwrap ( ) ; 973 + 974 + 975 + assert_eq ! ( latest[ "README.md" ] . summary, "expand readme" ) ; 976 + assert_eq ! ( latest[ "src" ] . summary, "initial import" ) ; 977 + } 978 + 979 + # [ test ] 980 + fn last_commit_per_entry_degrades_under_a_tight_cap ( ) { 981 + let fx = fixture ( ) ; 982 + let repo = open ( & fx) ; 983 + let main = repo. resolve_ref ( "main" ) . unwrap ( ) ; 984 + 985 + 986 + let capped = repo. last_commit_per_entry ( & main, Path :: new ( "" ) , 1 ) . unwrap ( ) ; 987 + assert_eq ! ( 988 + capped. get ( "README.md" ) . map ( |c| c. summary. as_str ( ) ) , 989 + Some ( "expand readme" ) 990 + ) ; 991 + assert ! ( 992 + !capped. contains_key ( "src" ) , 993 + "src is not reachable within the cap" 994 + ) ; 995 + } 996 + 997 + # [ test ] 691 998 fn commit_returns_full_detail ( ) { 692 999 let fx = fixture ( ) ; 693 1000 let repo = open ( & fx) ;