@@ -380,6 +380,205 @@ fn enqueue_cargo_husky_precommit_hook_jobs(sender: std::sync::mpsc::Sender<Job>)
380380 }
381381}
382382
383+ fn run_pre_push ( ) {
384+ use std:: collections:: { BTreeSet , HashSet } ;
385+
386+ println ! ( "{}" , "Running pre-push checks..." . cyan( ) . bold( ) ) ;
387+
388+ // Find the merge base with origin/main
389+ let merge_base_output = Command :: new ( "git" )
390+ . args ( [ "merge-base" , "HEAD" , "origin/main" ] )
391+ . output ( ) ;
392+
393+ let merge_base = match merge_base_output {
394+ Ok ( output) if output. status . success ( ) => {
395+ String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( )
396+ }
397+ _ => {
398+ warn ! ( "Failed to find merge base with origin/main, using HEAD" ) ;
399+ "HEAD" . to_string ( )
400+ }
401+ } ;
402+
403+ // Get the list of changed files
404+ let diff_output = Command :: new ( "git" )
405+ . args ( [ "diff" , "--name-only" , & format ! ( "{}...HEAD" , merge_base) ] )
406+ . output ( ) ;
407+
408+ let changed_files = match diff_output {
409+ Ok ( output) if output. status . success ( ) => String :: from_utf8_lossy ( & output. stdout )
410+ . lines ( )
411+ . map ( |s| s. to_string ( ) )
412+ . collect :: < Vec < _ > > ( ) ,
413+ Err ( e) => {
414+ error ! ( "Failed to get changed files: {}" , e) ;
415+ std:: process:: exit ( 1 ) ;
416+ }
417+ Ok ( output) => {
418+ error ! (
419+ "git diff failed: {}" ,
420+ String :: from_utf8_lossy( & output. stderr)
421+ ) ;
422+ std:: process:: exit ( 1 ) ;
423+ }
424+ } ;
425+
426+ if changed_files. is_empty ( ) {
427+ println ! ( "{}" , "No changes detected" . green( ) . bold( ) ) ;
428+ std:: process:: exit ( 0 ) ;
429+ }
430+
431+ // Find which crates are affected
432+ let mut affected_crates = HashSet :: new ( ) ;
433+
434+ for file in & changed_files {
435+ let path = Path :: new ( file) ;
436+
437+ // Find the crate directory by looking for Cargo.toml
438+ let mut current = path;
439+ while let Some ( parent) = current. parent ( ) {
440+ let cargo_toml = if parent. as_os_str ( ) . is_empty ( ) {
441+ PathBuf :: from ( "Cargo.toml" )
442+ } else {
443+ parent. join ( "Cargo.toml" )
444+ } ;
445+
446+ if cargo_toml. exists ( ) {
447+ // Read Cargo.toml to get the package name
448+ if let Ok ( content) = fs:: read_to_string ( & cargo_toml) {
449+ // Simple parsing: look for [package] section and name field
450+ let mut in_package = false ;
451+ for line in content. lines ( ) {
452+ let trimmed = line. trim ( ) ;
453+ if trimmed == "[package]" {
454+ in_package = true ;
455+ } else if trimmed. starts_with ( '[' ) {
456+ in_package = false ;
457+ } else if in_package && trimmed. starts_with ( "name" ) {
458+ if let Some ( name_part) = trimmed. split ( '=' ) . nth ( 1 ) {
459+ let name = name_part. trim ( ) . trim_matches ( '"' ) . trim_matches ( '\'' ) ;
460+ affected_crates. insert ( name. to_string ( ) ) ;
461+ break ;
462+ }
463+ }
464+ }
465+ }
466+ break ;
467+ }
468+
469+ if parent. as_os_str ( ) . is_empty ( ) {
470+ break ;
471+ }
472+ current = parent;
473+ }
474+ }
475+
476+ if affected_crates. is_empty ( ) {
477+ println ! ( "{}" , "No crates affected by changes" . yellow( ) ) ;
478+ std:: process:: exit ( 0 ) ;
479+ }
480+
481+ // Sort for consistent output
482+ let affected_crates: BTreeSet < _ > = affected_crates. into_iter ( ) . collect ( ) ;
483+
484+ println ! (
485+ "{} Affected crates: {}" ,
486+ "🔍" . cyan( ) ,
487+ affected_crates
488+ . iter( )
489+ . map( |s| s. as_str( ) )
490+ . collect:: <Vec <_>>( )
491+ . join( ", " )
492+ . yellow( )
493+ ) ;
494+
495+ let mut all_passed = true ;
496+
497+ for crate_name in & affected_crates {
498+ println ! (
499+ "\n {} Checking crate: {}" ,
500+ "📦" . cyan( ) ,
501+ crate_name. yellow( ) . bold( )
502+ ) ;
503+
504+ // Run clippy
505+ print ! ( " {} Running clippy... " , "🔍" . cyan( ) ) ;
506+ io:: stdout ( ) . flush ( ) . unwrap ( ) ;
507+ let clippy_status = Command :: new ( "cargo" )
508+ . args ( [ "clippy" , "-p" , crate_name, "--" , "-D" , "warnings" ] )
509+ . status ( ) ;
510+
511+ match clippy_status {
512+ Ok ( status) if status. success ( ) => {
513+ println ! ( "{}" , "passed" . green( ) ) ;
514+ }
515+ _ => {
516+ println ! ( "{}" , "failed" . red( ) ) ;
517+ all_passed = false ;
518+ }
519+ }
520+
521+ // Run tests
522+ print ! ( " {} Running tests... " , "🧪" . cyan( ) ) ;
523+ io:: stdout ( ) . flush ( ) . unwrap ( ) ;
524+ let test_status = Command :: new ( "cargo" )
525+ . args ( [ "test" , "-p" , crate_name] )
526+ . stdout ( Stdio :: null ( ) )
527+ . stderr ( Stdio :: null ( ) )
528+ . status ( ) ;
529+
530+ match test_status {
531+ Ok ( status) if status. success ( ) => {
532+ println ! ( "{}" , "passed" . green( ) ) ;
533+ }
534+ _ => {
535+ println ! ( "{}" , "failed" . red( ) ) ;
536+ all_passed = false ;
537+ }
538+ }
539+
540+ // Run doc tests
541+ print ! ( " {} Running doc tests... " , "📚" . cyan( ) ) ;
542+ io:: stdout ( ) . flush ( ) . unwrap ( ) ;
543+ let doctest_status = Command :: new ( "cargo" )
544+ . args ( [ "test" , "--doc" , "-p" , crate_name] )
545+ . stdout ( Stdio :: null ( ) )
546+ . stderr ( Stdio :: null ( ) )
547+ . status ( ) ;
548+
549+ match doctest_status {
550+ Ok ( status) if status. success ( ) => {
551+ println ! ( "{}" , "passed" . green( ) ) ;
552+ }
553+ Ok ( status) if status. code ( ) == Some ( 101 ) => {
554+ // Exit code 101 often means "no tests to run"
555+ println ! ( "{}" , "skipped (no lib)" . yellow( ) ) ;
556+ }
557+ _ => {
558+ println ! ( "{}" , "failed" . red( ) ) ;
559+ all_passed = false ;
560+ }
561+ }
562+ }
563+
564+ println ! ( ) ;
565+ if all_passed {
566+ println ! (
567+ "{} {}" ,
568+ "✅" . green( ) ,
569+ "All pre-push checks passed!" . green( ) . bold( )
570+ ) ;
571+ std:: process:: exit ( 0 ) ;
572+ } else {
573+ println ! (
574+ "{} {}" ,
575+ "❌" . red( ) ,
576+ "Some pre-push checks failed" . red( ) . bold( )
577+ ) ;
578+ std:: process:: exit ( 1 ) ;
579+ }
580+ }
581+
383582fn show_and_apply_jobs ( jobs : & mut [ Job ] ) {
384583 use std:: io:: { self , Write } ;
385584
@@ -456,6 +655,13 @@ fn main() {
456655 }
457656 }
458657
658+ // Parse CLI arguments
659+ let args: Vec < String > = std:: env:: args ( ) . collect ( ) ;
660+ if args. len ( ) > 1 && args[ 1 ] == "pre-push" {
661+ run_pre_push ( ) ;
662+ return ;
663+ }
664+
459665 let staged_files = match collect_staged_files ( ) {
460666 Ok ( sf) => sf,
461667 Err ( e) => {
0 commit comments