@@ -9,7 +9,7 @@ use std::sync::{Arc, OnceLock};
99use anyhow:: { Context , Result } ;
1010use clap:: ValueEnum ;
1111use prek_consts:: PRE_COMMIT_HOOKS_YAML ;
12- use rustc_hash:: { FxBuildHasher , FxHashMap , FxHashSet } ;
12+ use rustc_hash:: FxHashMap ;
1313use serde:: { Deserialize , Serialize } ;
1414use tempfile:: TempDir ;
1515use thiserror:: Error ;
@@ -330,11 +330,7 @@ impl HookBuilder {
330330 let pass_filenames = options. pass_filenames . unwrap_or ( true ) ;
331331 let require_serial = options. require_serial . unwrap_or ( false ) ;
332332 let verbose = options. verbose . unwrap_or ( false ) ;
333- let additional_dependencies = options
334- . additional_dependencies
335- . unwrap_or_default ( )
336- . into_iter ( )
337- . collect :: < FxHashSet < _ > > ( ) ;
333+ let additional_dependencies = options. additional_dependencies . unwrap_or_default ( ) ;
338334
339335 let language_request = LanguageRequest :: parse ( self . hook_spec . language , & language_version)
340336 . map_err ( |e| Error :: Hook {
@@ -482,7 +478,7 @@ pub(crate) struct Hook {
482478 project : Arc < Project > ,
483479 repo : Arc < Repo > ,
484480 // Cached computed dependencies.
485- dependencies : OnceLock < FxHashSet < String > > ,
481+ dependencies : OnceLock < Vec < String > > ,
486482
487483 /// The index of the hook defined in the configuration file.
488484 pub idx : usize ,
@@ -496,7 +492,7 @@ pub(crate) struct Hook {
496492 pub types : Vec < String > ,
497493 pub types_or : Vec < String > ,
498494 pub exclude_types : Vec < String > ,
499- pub additional_dependencies : FxHashSet < String > ,
495+ pub additional_dependencies : Vec < String > ,
500496 pub args : Vec < String > ,
501497 pub env : FxHashMap < String , String > ,
502498 pub always_run : bool ,
@@ -558,7 +554,7 @@ impl Hook {
558554 ///
559555 /// For remote hooks, the repo URL is included to avoid reusing an environment created
560556 /// from a different remote repository.
561- pub ( crate ) fn env_key_dependencies ( & self ) -> & FxHashSet < String > {
557+ pub ( crate ) fn env_key_dependencies ( & self ) -> & [ String ] {
562558 if !self . is_remote ( ) {
563559 return & self . additional_dependencies ;
564560 }
@@ -586,10 +582,11 @@ impl Hook {
586582 ///
587583 /// For remote hooks, this includes the local path to the cloned repository so that
588584 /// installers can install the hook's package/project itself.
589- pub ( crate ) fn install_dependencies ( & self ) -> Cow < ' _ , FxHashSet < String > > {
585+ pub ( crate ) fn install_dependencies ( & self ) -> Cow < ' _ , [ String ] > {
590586 if let Some ( repo_path) = self . repo_path ( ) {
591- let mut deps = self . additional_dependencies . clone ( ) ;
592- deps. insert ( repo_path. to_string_lossy ( ) . to_string ( ) ) ;
587+ let mut deps = Vec :: with_capacity ( self . additional_dependencies . len ( ) + 1 ) ;
588+ deps. push ( repo_path. to_string_lossy ( ) . to_string ( ) ) ;
589+ deps. extend ( self . additional_dependencies . iter ( ) . cloned ( ) ) ;
593590 Cow :: Owned ( deps)
594591 } else {
595592 Cow :: Borrowed ( & self . additional_dependencies )
@@ -600,7 +597,7 @@ impl Hook {
600597#[ derive( Debug , Clone ) ]
601598pub ( crate ) struct HookEnvKey {
602599 pub ( crate ) language : Language ,
603- pub ( crate ) dependencies : FxHashSet < String > ,
600+ pub ( crate ) dependencies : Vec < String > ,
604601 pub ( crate ) language_request : LanguageRequest ,
605602}
606603
@@ -609,25 +606,24 @@ pub(crate) struct HookEnvKey {
609606#[ derive( Debug , Clone , Copy ) ]
610607pub ( crate ) struct HookEnvKeyRef < ' a > {
611608 pub ( crate ) language : Language ,
612- pub ( crate ) dependencies : & ' a FxHashSet < String > ,
609+ pub ( crate ) dependencies : & ' a [ String ] ,
613610 pub ( crate ) language_request : & ' a LanguageRequest ,
614611}
615612
616- /// Builds the dependency set used to identify a hook environment.
613+ /// Builds the dependency list used to identify a hook environment.
617614///
618615/// For remote hooks, `remote_repo_dependency` is included so environments from different
619616/// repositories are not reused accidentally.
620617fn env_key_dependencies (
621- additional_dependencies : & FxHashSet < String > ,
618+ additional_dependencies : & [ String ] ,
622619 remote_repo_dependency : Option < & str > ,
623- ) -> FxHashSet < String > {
624- let mut deps = FxHashSet :: with_capacity_and_hasher (
620+ ) -> Vec < String > {
621+ let mut deps = Vec :: with_capacity (
625622 additional_dependencies. len ( ) + usize:: from ( remote_repo_dependency. is_some ( ) ) ,
626- FxBuildHasher ,
627623 ) ;
628624 deps. extend ( additional_dependencies. iter ( ) . cloned ( ) ) ;
629625 if let Some ( dep) = remote_repo_dependency {
630- deps. insert ( dep. to_string ( ) ) ;
626+ deps. push ( dep. to_string ( ) ) ;
631627 }
632628 deps
633629}
@@ -636,7 +632,7 @@ fn env_key_dependencies(
636632/// environment described by [`InstallInfo`].
637633fn matches_install_info (
638634 language : Language ,
639- dependencies : & FxHashSet < String > ,
635+ dependencies : & [ String ] ,
640636 language_request : & LanguageRequest ,
641637 info : & InstallInfo ,
642638) -> bool {
@@ -674,11 +670,11 @@ impl HookEnvKey {
674670 )
675671 } ) ?;
676672
677- let additional_dependencies: FxHashSet < String > = hook_spec
673+ let additional_dependencies: Vec < String > = hook_spec
678674 . options
679675 . additional_dependencies
680- . as_ref ( )
681- . map_or_else ( FxHashSet :: default , |deps| deps . iter ( ) . cloned ( ) . collect ( ) ) ;
676+ . clone ( )
677+ . unwrap_or_default ( ) ;
682678
683679 let dependencies = env_key_dependencies ( & additional_dependencies, remote_repo_dependency) ;
684680
@@ -786,7 +782,7 @@ impl InstalledHook {
786782pub ( crate ) struct InstallInfo {
787783 pub ( crate ) language : Language ,
788784 pub ( crate ) language_version : semver:: Version ,
789- pub ( crate ) dependencies : FxHashSet < String > ,
785+ pub ( crate ) dependencies : Vec < String > ,
790786 pub ( crate ) env_path : PathBuf ,
791787 pub ( crate ) toolchain : PathBuf ,
792788 extra : FxHashMap < String , String > ,
@@ -811,7 +807,7 @@ impl Clone for InstallInfo {
811807impl InstallInfo {
812808 pub ( crate ) fn new (
813809 language : Language ,
814- dependencies : FxHashSet < String > ,
810+ dependencies : Vec < String > ,
815811 hooks_dir : & Path ,
816812 ) -> Result < Self , Error > {
817813 let env_path = tempfile:: Builder :: new ( )
@@ -1004,7 +1000,7 @@ mod tests {
10041000 ],
10051001 types_or: [],
10061002 exclude_types: [],
1007- additional_dependencies: {} ,
1003+ additional_dependencies: [] ,
10081004 args: [
10091005 "--flag",
10101006 ],
@@ -1039,4 +1035,83 @@ mod tests {
10391035
10401036 Ok ( ( ) )
10411037 }
1038+
1039+ #[ tokio:: test]
1040+ async fn hook_builder_preserves_additional_dependency_order_and_duplicates ( ) -> Result < ( ) > {
1041+ let temp = tempfile:: tempdir ( ) ?;
1042+ let config_path = temp. path ( ) . join ( PRE_COMMIT_CONFIG_YAML ) ;
1043+ fs_err:: write ( & config_path, "repos: []\n " ) ?;
1044+
1045+ let project = Arc :: new ( Project :: from_config_file (
1046+ Cow :: Borrowed ( & config_path) ,
1047+ None ,
1048+ ) ?) ;
1049+ let repo = Arc :: new ( Repo :: Local { hooks : vec ! [ ] } ) ;
1050+
1051+ let expected = vec ! [
1052+ "--index-url" . to_string( ) ,
1053+ "https://pypi.org/simple" . to_string( ) ,
1054+ "pyecho-cli" . to_string( ) ,
1055+ "pyecho-cli" . to_string( ) ,
1056+ ] ;
1057+
1058+ let hook_spec = HookSpec {
1059+ id : "ordered-deps" . to_string ( ) ,
1060+ name : "ordered-deps" . to_string ( ) ,
1061+ entry : "python -c 'print(1)'" . to_string ( ) ,
1062+ language : Language :: Python ,
1063+ priority : None ,
1064+ options : HookOptions {
1065+ additional_dependencies : Some ( expected. clone ( ) ) ,
1066+ ..Default :: default ( )
1067+ } ,
1068+ } ;
1069+
1070+ let hook = HookBuilder :: new ( project. clone ( ) , repo. clone ( ) , hook_spec, 0 )
1071+ . build ( )
1072+ . await ?;
1073+ assert_eq ! ( hook. additional_dependencies, expected) ;
1074+
1075+ let hook_a = HookBuilder :: new (
1076+ project. clone ( ) ,
1077+ repo. clone ( ) ,
1078+ HookSpec {
1079+ id : "a" . to_string ( ) ,
1080+ name : "a" . to_string ( ) ,
1081+ entry : "python -c 'print(1)'" . to_string ( ) ,
1082+ language : Language :: Python ,
1083+ priority : None ,
1084+ options : HookOptions {
1085+ additional_dependencies : Some ( vec ! [ "foo" . to_string( ) , "bar" . to_string( ) ] ) ,
1086+ ..Default :: default ( )
1087+ } ,
1088+ } ,
1089+ 1 ,
1090+ )
1091+ . build ( )
1092+ . await ?;
1093+
1094+ let hook_b = HookBuilder :: new (
1095+ project,
1096+ repo,
1097+ HookSpec {
1098+ id : "b" . to_string ( ) ,
1099+ name : "b" . to_string ( ) ,
1100+ entry : "python -c 'print(1)'" . to_string ( ) ,
1101+ language : Language :: Python ,
1102+ priority : None ,
1103+ options : HookOptions {
1104+ additional_dependencies : Some ( vec ! [ "bar" . to_string( ) , "foo" . to_string( ) ] ) ,
1105+ ..Default :: default ( )
1106+ } ,
1107+ } ,
1108+ 2 ,
1109+ )
1110+ . build ( )
1111+ . await ?;
1112+
1113+ assert_ne ! ( hook_a. env_key_dependencies( ) , hook_b. env_key_dependencies( ) ) ;
1114+
1115+ Ok ( ( ) )
1116+ }
10421117}
0 commit comments