88//! (str or bytes) only happens in CI or when libgit2 cannot be used to initialize a
99//! repository.
1010
11- use std:: { ops:: RangeInclusive , path:: PathBuf } ;
11+ use std:: { fmt :: Display , ops:: RangeInclusive , path:: PathBuf } ;
1212
1313use anyhow:: { Context , Result } ;
1414// non-std crates
@@ -33,50 +33,95 @@ pub fn open_repo(path: &str) -> Result<Repository, Error> {
3333///
3434/// The optionally specified `depth` can be used to traverse the tree a number of times
3535/// since the current `"HEAD"`.
36- fn get_sha ( repo : & Repository , depth : Option < u32 > ) -> Result < git2:: Object < ' _ > , Error > {
36+ fn get_sha < ' d , T : Display > (
37+ repo : & ' d Repository ,
38+ depth : & Option < T > ,
39+ ) -> Result < git2:: Object < ' d > , Error > {
3740 match depth {
38- Some ( int) => repo. revparse_single ( format ! ( "HEAD~{}" , int) . as_str ( ) ) ,
41+ Some ( base) => {
42+ if base. to_string ( ) . parse :: < u32 > ( ) . is_ok ( ) {
43+ repo. revparse_single ( format ! ( "HEAD~{}" , base) . as_str ( ) )
44+ } else {
45+ repo. revparse_single ( format ! ( "{base}" ) . as_str ( ) )
46+ }
47+ }
3948 None => repo. revparse_single ( "HEAD" ) ,
4049 }
4150}
4251
4352/// Fetch the [`git2::Diff`] about a given [`git2::Repository`].
4453///
4554/// This is actually not used in CI for file permissions and ownership reasons.
46- /// Rather this is only (supposed to be) used when executed on a local developer
55+ /// Rather, this is only (supposed to be) used when executed on a local developer
4756/// machine.
4857///
49- /// If there are files staged for a commit, then the resulting [`Diff`] will describe
50- /// the staged changes. However, if there are no staged changes, then the last commit's
51- /// [`Diff`] is returned.
52- pub fn get_diff ( repo : & ' _ Repository ) -> Result < git2:: Diff < ' _ > > {
53- let head = get_sha ( repo, None ) . unwrap ( ) . peel_to_tree ( ) . unwrap ( ) ;
54- let mut has_staged_files = false ;
55- for entry in repo. statuses ( None ) . unwrap ( ) . iter ( ) {
56- if entry. status ( ) . bits ( )
57- & ( git2:: Status :: INDEX_NEW . bits ( )
58- | git2:: Status :: INDEX_MODIFIED . bits ( )
59- | git2:: Status :: INDEX_RENAMED . bits ( ) )
60- > 0
61- {
62- has_staged_files = true ;
63- break ;
64- }
65- }
58+ /// ## Using `diff_base` and `ignore_index`
59+ ///
60+ /// The `diff_base` is a commit or ref to use as the base of the diff.
61+ /// Use `ignore_index` to exclude any staged changes in the local index.
62+ ///
63+ /// | Parameter Value | Git index state | Scope of diff |
64+ /// |-----------------|-----------------|----------------|
65+ /// | `None` (the default) | No staged changes | `HEAD~1..HEAD` |
66+ /// | `None` (the default) | Has staged changes | `HEAD..index` |
67+ /// | `int` (eg `2`) or `str` (eg `HEAD~2`) | No staged changes | `HEAD~2..HEAD` |
68+ /// | `int` (eg `2`) or `str` (eg `HEAD~2`) | Has staged changes | `HEAD~2..index` |
69+ pub fn get_diff < ' d , T : Display > (
70+ repo : & ' d Repository ,
71+ diff_base : & Option < T > ,
72+ ignore_index : bool ,
73+ ) -> Result < git2:: Diff < ' d > > {
74+ let use_staged_files = if ignore_index {
75+ false
76+ } else {
77+ // check if there are staged file changes
78+ repo. statuses ( None )
79+ . with_context ( || "Could not get repo statuses" ) ?
80+ . iter ( )
81+ . any ( |entry| {
82+ entry. status ( ) . bits ( )
83+ & ( git2:: Status :: INDEX_NEW . bits ( )
84+ | git2:: Status :: INDEX_MODIFIED . bits ( )
85+ | git2:: Status :: INDEX_RENAMED . bits ( ) )
86+ > 0
87+ } )
88+ } ;
89+ let base = if diff_base. is_some ( ) {
90+ // diff base is specified (regardless of staged changes)
91+ get_sha ( repo, diff_base)
92+ } else if !use_staged_files && !ignore_index {
93+ // diff base is unspecified, when the repo has
94+ // no staged changes (and they are not ignored),
95+ // then focus on just the last commit
96+ get_sha ( repo, & Some ( 1 ) )
97+ } else {
98+ // diff base is unspecified and there are staged changes, so
99+ // let base be set to HEAD.
100+ get_sha ( repo, & None :: < u8 > )
101+ } ?
102+ . peel_to_tree ( ) ?;
66103
67104 // RARE BUG when `head` is the first commit in the repo! Affects local-only runs.
68- // > panicked at cpp-linter\src\git.rs:73:43:
69- // > called `Result::unwrap()` on an `Err` value:
70105 // > Error { code: -3, class: 3, message: "parent 0 does not exist" }
71- if has_staged_files {
72- // get diff for staged files only
73- repo. diff_tree_to_index ( Some ( & head) , None , None )
74- . with_context ( || "Could not get diff for current changes in local repo index" )
106+ if use_staged_files {
107+ // get diff including staged files
108+ repo. diff_tree_to_index ( Some ( & base) , None , None )
109+ . with_context ( || {
110+ format ! (
111+ "Could not get diff for {}..index" ,
112+ & base. id( ) . to_string( ) [ ..7 ]
113+ )
114+ } )
75115 } else {
76- // get diff for last commit only
77- let base = get_sha ( repo, Some ( 1 ) ) . unwrap ( ) . peel_to_tree ( ) . unwrap ( ) ;
116+ // get diff for range of commits between base..HEAD
117+ let head = get_sha ( repo, & None :: < u8 > ) ? . peel_to_tree ( ) ? ;
78118 repo. diff_tree_to_tree ( Some ( & base) , Some ( & head) , None )
79- . with_context ( || "Could not get diff for last commit" )
119+ . with_context ( || {
120+ format ! (
121+ "Could not get diff for {}..HEAD" ,
122+ & base. id( ) . to_string( ) [ ..7 ]
123+ )
124+ } )
80125 }
81126}
82127
@@ -105,8 +150,10 @@ fn parse_patch(patch: &Patch) -> (Vec<u32>, Vec<RangeInclusive<u32>>) {
105150
106151/// Parses a given [`git2::Diff`] and returns a list of [`FileObj`]s.
107152///
108- /// The specified list of `extensions`, `ignored` and `not_ignored` files are used as
109- /// filters to expedite the process and only focus on the data cpp_linter can use.
153+ /// The `lines_changed_only`, parameter is used to expedite the process and only
154+ /// focus on files that have relevant changes. The `file_filter` parameter applies
155+ /// a filter to only include source files (or ignored files) based on the
156+ /// extensions and ignore patterns specified.
110157pub fn parse_diff (
111158 diff : & git2:: Diff ,
112159 file_filter : & FileFilter ,
@@ -393,19 +440,30 @@ mod test {
393440 fs:: read,
394441 } ;
395442
396- use git2:: build:: CheckoutBuilder ;
397- use git2:: { ApplyLocation , Diff , IndexAddOption , Repository } ;
443+ use git2:: { ApplyLocation , Diff , IndexAddOption , Repository , build:: CheckoutBuilder } ;
444+ use tempfile:: { TempDir , tempdir} ;
445+
446+ use super :: get_sha;
447+ use crate :: {
448+ cli:: LinesChangedOnly ,
449+ common_fs:: FileFilter ,
450+ rest_api:: { RestApiClient , github:: GithubApiClient } ,
451+ } ;
452+
453+ const TEST_REPO_URL : & str = "https://github.com/cpp-linter/cpp-linter" ;
398454
399455 // used to setup a testing stage
400- fn clone_repo ( url : & str , sha : & str , path : & str , patch_path : Option < & str > ) {
401- let repo = Repository :: clone ( url, path) . unwrap ( ) ;
402- let commit = repo. revparse_single ( sha) . unwrap ( ) ;
403- repo. checkout_tree (
404- & commit,
405- Some ( CheckoutBuilder :: new ( ) . force ( ) . recreate_missing ( true ) ) ,
406- )
407- . unwrap ( ) ;
408- repo. set_head_detached ( commit. id ( ) ) . unwrap ( ) ;
456+ fn clone_repo ( sha : Option < & str > , path : & str , patch_path : Option < & str > ) -> Repository {
457+ let repo = Repository :: clone ( TEST_REPO_URL , path) . unwrap ( ) ;
458+ if let Some ( sha) = sha {
459+ let commit = repo. revparse_single ( sha) . unwrap ( ) ;
460+ repo. checkout_tree (
461+ & commit,
462+ Some ( CheckoutBuilder :: new ( ) . force ( ) . recreate_missing ( true ) ) ,
463+ )
464+ . unwrap ( ) ;
465+ repo. set_head_detached ( commit. id ( ) ) . unwrap ( ) ;
466+ }
409467 if let Some ( patch) = patch_path {
410468 let diff = Diff :: from_buffer ( & read ( patch) . unwrap ( ) ) . unwrap ( ) ;
411469 repo. apply ( & diff, ApplyLocation :: Both , None ) . unwrap ( ) ;
@@ -415,16 +473,9 @@ mod test {
415473 . unwrap ( ) ;
416474 index. write ( ) . unwrap ( ) ;
417475 }
476+ repo
418477 }
419478
420- use tempfile:: { TempDir , tempdir} ;
421-
422- use crate :: {
423- cli:: LinesChangedOnly ,
424- common_fs:: FileFilter ,
425- rest_api:: { RestApiClient , github:: GithubApiClient } ,
426- } ;
427-
428479 fn get_temp_dir ( ) -> TempDir {
429480 let tmp = tempdir ( ) . unwrap ( ) ;
430481 println ! ( "Using temp folder at {:?}" , tmp. path( ) ) ;
@@ -436,11 +487,10 @@ mod test {
436487 extensions : & [ String ] ,
437488 tmp : & TempDir ,
438489 patch_path : Option < & str > ,
490+ ignore_staged : bool ,
439491 ) -> Vec < crate :: common_fs:: FileObj > {
440- let url = "https://github.com/cpp-linter/cpp-linter" ;
441492 clone_repo (
442- url,
443- sha,
493+ Some ( sha) ,
444494 tmp. path ( ) . as_os_str ( ) . to_str ( ) . unwrap ( ) ,
445495 patch_path,
446496 ) ;
@@ -453,7 +503,12 @@ mod test {
453503 }
454504 rest_api_client
455505 . unwrap ( )
456- . get_list_of_changed_files ( & file_filter, & LinesChangedOnly :: Off )
506+ . get_list_of_changed_files (
507+ & file_filter,
508+ & LinesChangedOnly :: Off ,
509+ if ignore_staged { & Some ( 0 ) } else { & None :: < u8 > } ,
510+ ignore_staged,
511+ )
457512 . await
458513 . unwrap ( )
459514 }
@@ -465,7 +520,7 @@ mod test {
465520 let cur_dir = current_dir ( ) . unwrap ( ) ;
466521 let tmp = get_temp_dir ( ) ;
467522 let extensions = vec ! [ "cpp" . to_string( ) , "hpp" . to_string( ) ] ;
468- let files = checkout_cpp_linter_py_repo ( sha, & extensions, & tmp, None ) . await ;
523+ let files = checkout_cpp_linter_py_repo ( sha, & extensions, & tmp, None , false ) . await ;
469524 println ! ( "files = {:?}" , files) ;
470525 assert ! ( files. is_empty( ) ) ;
471526 set_current_dir ( cur_dir) . unwrap ( ) ; // prep to delete temp_folder
@@ -479,7 +534,7 @@ mod test {
479534 let cur_dir = current_dir ( ) . unwrap ( ) ;
480535 let tmp = get_temp_dir ( ) ;
481536 let extensions = vec ! [ "cpp" . to_string( ) , "hpp" . to_string( ) ] ;
482- let files = checkout_cpp_linter_py_repo ( sha, & extensions. clone ( ) , & tmp, None ) . await ;
537+ let files = checkout_cpp_linter_py_repo ( sha, & extensions. clone ( ) , & tmp, None , false ) . await ;
483538 println ! ( "files = {:?}" , files) ;
484539 assert ! ( files. len( ) >= 2 ) ;
485540 for file in files {
@@ -503,6 +558,7 @@ mod test {
503558 & extensions. clone ( ) ,
504559 & tmp,
505560 Some ( "tests/git_status_test_assets/cpp-linter/cpp-linter/test_git_lib.patch" ) ,
561+ false ,
506562 )
507563 . await ;
508564 println ! ( "files = {:?}" , files) ;
@@ -515,4 +571,37 @@ mod test {
515571 set_current_dir ( cur_dir) . unwrap ( ) ; // prep to delete temp_folder
516572 drop ( tmp) ; // delete temp_folder
517573 }
574+
575+ #[ tokio:: test]
576+ async fn with_ignored_staged_changes ( ) {
577+ // commit with no modified C/C++ sources
578+ let sha = "0c236809891000b16952576dc34de082d7a40bf3" ;
579+ let cur_dir = current_dir ( ) . unwrap ( ) ;
580+ let tmp = get_temp_dir ( ) ;
581+ let extensions = vec ! [ "cpp" . to_string( ) , "hpp" . to_string( ) ] ;
582+ let files = checkout_cpp_linter_py_repo (
583+ sha,
584+ & extensions. clone ( ) ,
585+ & tmp,
586+ Some ( "tests/git_status_test_assets/cpp-linter/cpp-linter/test_git_lib.patch" ) ,
587+ true ,
588+ )
589+ . await ;
590+ println ! ( "files = {:?}" , files) ;
591+ assert ! ( files. is_empty( ) ) ;
592+ set_current_dir ( cur_dir) . unwrap ( ) ; // prep to delete temp_folder
593+ drop ( tmp) ; // delete temp_folder
594+ }
595+
596+ #[ test]
597+ fn repo_get_sha ( ) {
598+ let tmp_dir = get_temp_dir ( ) ;
599+ let repo = clone_repo ( None , tmp_dir. path ( ) . to_str ( ) . unwrap ( ) , None ) ;
600+ for ( ours, theirs) in [ ( None :: < u8 > , "HEAD" ) , ( Some ( 2 ) , "HEAD~2" ) ] {
601+ let our_obj = get_sha ( & repo, & ours) . unwrap ( ) ;
602+ let their_obj = get_sha ( & repo, & Some ( theirs) ) . unwrap ( ) ;
603+ assert_eq ! ( our_obj. id( ) , their_obj. id( ) ) ;
604+ }
605+ drop ( tmp_dir) ; // delete temp_folder
606+ }
518607}
0 commit comments