@@ -394,76 +394,89 @@ fn check_repo_state(
394
394
src_files : & [ PathBuf ] ,
395
395
repo : & git2:: Repository ,
396
396
) -> CargoResult < Option < String > > {
397
- let workdir = repo. workdir ( ) . unwrap ( ) ;
398
-
399
- let mut sub_repos = Vec :: new ( ) ;
400
- open_submodules ( repo, & mut sub_repos) ?;
401
- // Sort so that longest paths are first, to check nested submodules first.
402
- sub_repos. sort_unstable_by ( |a, b| b. 0 . as_os_str ( ) . len ( ) . cmp ( & a. 0 . as_os_str ( ) . len ( ) ) ) ;
403
- let submodule_dirty = |path : & Path | -> bool {
404
- sub_repos
405
- . iter ( )
406
- . filter ( |( sub_path, _sub_repo) | path. starts_with ( sub_path) )
407
- . any ( |( sub_path, sub_repo) | {
408
- let relative = path. strip_prefix ( sub_path) . unwrap ( ) ;
409
- sub_repo
410
- . status_file ( relative)
411
- . map ( |status| status != git2:: Status :: CURRENT )
412
- . unwrap_or ( false )
413
- } )
414
- } ;
415
-
416
- let dirty = src_files
397
+ // This is a collection of any dirty or untracked files. This covers:
398
+ // - new/modified/deleted/renamed/type change (index or worktree)
399
+ // - untracked files (which are "new" worktree files)
400
+ // - ignored (in case the user has an `include` directive that
401
+ // conflicts with .gitignore).
402
+ let mut dirty_files = Vec :: new ( ) ;
403
+ collect_statuses ( repo, & mut dirty_files) ?;
404
+ // Include each submodule so that the error message can provide
405
+ // specifically *which* files in a submodule are modified.
406
+ status_submodules ( repo, & mut dirty_files) ?;
407
+
408
+ // Find the intersection of dirty in git, and the src_files that would
409
+ // be packaged. This is a lazy n^2 check, but seems fine with
410
+ // thousands of files.
411
+ let dirty_src_files: Vec < String > = src_files
417
412
. iter ( )
418
- . filter ( |file| {
419
- let relative = file. strip_prefix ( workdir) . unwrap ( ) ;
420
- if let Ok ( status) = repo. status_file ( relative) {
421
- if status == git2:: Status :: CURRENT {
422
- false
423
- } else if relative. file_name ( ) . and_then ( |s| s. to_str ( ) ) . unwrap_or ( "" )
424
- == "Cargo.lock"
425
- {
426
- // It is OK to include this file even if it is ignored.
427
- status != git2:: Status :: IGNORED
428
- } else {
429
- true
430
- }
431
- } else {
432
- submodule_dirty ( file)
433
- }
434
- } )
413
+ . filter ( |src_file| dirty_files. iter ( ) . any ( |path| src_file. starts_with ( path) ) )
435
414
. map ( |path| {
436
415
path. strip_prefix ( p. root ( ) )
437
416
. unwrap_or ( path)
438
417
. display ( )
439
418
. to_string ( )
440
419
} )
441
- . collect :: < Vec < _ > > ( ) ;
442
- if dirty . is_empty ( ) {
420
+ . collect ( ) ;
421
+ if dirty_src_files . is_empty ( ) {
443
422
let rev_obj = repo. revparse_single ( "HEAD" ) ?;
444
423
Ok ( Some ( rev_obj. id ( ) . to_string ( ) ) )
445
424
} else {
446
425
anyhow:: bail!(
447
426
"{} files in the working directory contain changes that were \
448
427
not yet committed into git:\n \n {}\n \n \
449
428
to proceed despite this and include the uncommitted changes, pass the `--allow-dirty` flag",
450
- dirty . len( ) ,
451
- dirty . join( "\n " )
429
+ dirty_src_files . len( ) ,
430
+ dirty_src_files . join( "\n " )
452
431
)
453
432
}
454
433
}
455
434
456
- /// Helper to recursively open all submodules.
457
- fn open_submodules (
435
+ // Helper to collect dirty statuses for a single repo.
436
+ fn collect_statuses (
437
+ repo : & git2:: Repository ,
438
+ dirty_files : & mut Vec < PathBuf > ,
439
+ ) -> CargoResult < ( ) > {
440
+ let mut status_opts = git2:: StatusOptions :: new ( ) ;
441
+ // Exclude submodules, as they are being handled manually by recursing
442
+ // into each one so that details about specific files can be
443
+ // retrieved.
444
+ status_opts
445
+ . exclude_submodules ( true )
446
+ . include_ignored ( true )
447
+ . include_untracked ( true ) ;
448
+ let repo_statuses = repo. statuses ( Some ( & mut status_opts) ) . with_context ( || {
449
+ format ! (
450
+ "failed to retrieve git status from repo {}" ,
451
+ repo. path( ) . display( )
452
+ )
453
+ } ) ?;
454
+ let workdir = repo. workdir ( ) . unwrap ( ) ;
455
+ let this_dirty = repo_statuses. iter ( ) . filter_map ( |entry| {
456
+ let path = entry. path ( ) . expect ( "valid utf-8 path" ) ;
457
+ if path. ends_with ( "Cargo.lock" ) && entry. status ( ) == git2:: Status :: IGNORED {
458
+ // It is OK to include Cargo.lock even if it is ignored.
459
+ return None ;
460
+ }
461
+ // Use an absolute path, so that comparing paths is easier
462
+ // (particularly with submodules).
463
+ Some ( workdir. join ( path) )
464
+ } ) ;
465
+ dirty_files. extend ( this_dirty) ;
466
+ Ok ( ( ) )
467
+ }
468
+
469
+ // Helper to collect dirty statuses while recursing into submodules.
470
+ fn status_submodules (
458
471
repo : & git2:: Repository ,
459
- sub_repos : & mut Vec < ( PathBuf , git2 :: Repository ) > ,
472
+ dirty_files : & mut Vec < PathBuf > ,
460
473
) -> CargoResult < ( ) > {
461
474
for submodule in repo. submodules ( ) ? {
462
475
// Ignore submodules that don't open, they are probably not initialized.
463
476
// If its files are required, then the verification step should fail.
464
477
if let Ok ( sub_repo) = submodule. open ( ) {
465
- open_submodules ( & sub_repo, sub_repos ) ?;
466
- sub_repos . push ( ( sub_repo. workdir ( ) . unwrap ( ) . to_owned ( ) , sub_repo ) ) ;
478
+ status_submodules ( & sub_repo, dirty_files ) ?;
479
+ collect_statuses ( & sub_repo, dirty_files ) ? ;
467
480
}
468
481
}
469
482
Ok ( ( ) )
0 commit comments