@@ -29,77 +29,16 @@ async fn get_head_rev(repo: &Path) -> Result<String> {
2929 Ok ( head_rev)
3030}
3131
32- async fn clone_and_commit ( repo_path : & Path , head_rev : & str , tmp_dir : & Path ) -> Result < PathBuf > {
33- let shadow = tmp_dir. join ( "shadow-repo" ) ;
34- git:: git_cmd ( ) ?
35- . arg ( "clone" )
36- . arg ( repo_path)
37- . arg ( & shadow)
38- . output ( )
39- . await ?;
40- git:: git_cmd ( ) ?
41- . arg ( "checkout" )
42- . arg ( head_rev)
43- . arg ( "-b" )
44- . arg ( "_prek_tmp" )
45- . current_dir ( & shadow)
46- . output ( )
47- . await ?;
48-
49- let index_path = shadow. join ( ".git/index" ) ;
50- let objects_path = shadow. join ( ".git/objects" ) ;
51-
52- let staged_files = git:: get_staged_files ( repo_path) . await ?;
53- if !staged_files. is_empty ( ) {
54- git:: git_cmd ( ) ?
55- . arg ( "add" )
56- . arg ( "--" )
57- . file_args ( & staged_files)
58- . current_dir ( repo_path)
59- . env ( "GIT_INDEX_FILE" , & index_path)
60- . env ( "GIT_OBJECT_DIRECTORY" , & objects_path)
61- . output ( )
62- . await ?;
63- }
64-
65- let mut add_u_cmd = git:: git_cmd ( ) ?;
66- add_u_cmd
67- . arg ( "add" )
68- . arg ( "--update" ) // Update tracked files
69- . current_dir ( repo_path)
70- . env ( "GIT_INDEX_FILE" , & index_path)
71- . env ( "GIT_OBJECT_DIRECTORY" , & objects_path)
72- . output ( )
73- . await ?;
74-
75- git:: git_cmd ( ) ?
76- . arg ( "commit" )
77- . arg ( "-m" )
78- . arg ( "Temporary commit by prek try-repo" )
79- . arg ( "--no-gpg-sign" )
80- . arg ( "--no-edit" )
81- . arg ( "--no-verify" )
82- . current_dir ( & shadow)
83- . env ( "GIT_AUTHOR_NAME" , "prek test" )
84- . env ( "GIT_AUTHOR_EMAIL" , "test@example.com" )
85- . env ( "GIT_COMMITTER_NAME" , "prek test" )
86- . env ( "GIT_COMMITTER_EMAIL" , "test@example.com" )
87- . output ( )
88- . await ?;
89-
90- Ok ( shadow)
91- }
92-
9332struct PreparedRepo < ' a > {
9433 runtime_source : Cow < ' a , str > ,
9534 display_source : Option < & ' a str > ,
9635 rev : String ,
9736}
9837
9938async fn prepare_repo < ' a > (
39+ store : & Store ,
10040 repo : & ' a str ,
10141 rev : Option < & str > ,
102- tmp_dir : & Path ,
10342) -> Result < PreparedRepo < ' a > > {
10443 let repo_path = Path :: new ( repo) ;
10544 let is_local = repo_path. is_dir ( ) ;
@@ -143,15 +82,23 @@ async fn prepare_repo<'a>(
14382 . to_string ( )
14483 } ;
14584
146- // If repo is a local repo with uncommitted changes, create a shadow repo to commit the changes.
85+ // Persist a deterministic synthetic commit in the shared source. The logical source remains
86+ // the canonical local path, so identical dirty trees get the same repo and environment keys.
14787 if is_local && git:: has_diff ( "HEAD" , repo_path) . await ? {
14888 warn_user ! ( "Creating temporary repo with uncommitted changes..." ) ;
149- let shadow = clone_and_commit ( repo_path, & head_rev, tmp_dir) . await ?;
150- let head_rev = get_head_rev ( & shadow) . await ?;
89+ let source = store. repo_source_path ( runtime_source. as_ref ( ) ) ;
90+ let _source_lock = store. repo_source_lock ( runtime_source. as_ref ( ) ) . await ?;
91+ git:: ensure_bare_repo ( runtime_source. as_ref ( ) , & source) . await ?;
92+ let head_rev =
93+ git:: fetch_repo_source_revision ( & source, & head_rev, git:: TerminalPrompt :: Disabled )
94+ . await ?;
95+ let snapshot =
96+ git:: create_repo_snapshot ( & source, repo_path, head_rev. commit ( ) , & store. scratch_path ( ) )
97+ . await ?;
15198 Ok ( PreparedRepo {
152- runtime_source : Cow :: Owned ( shadow . to_string_lossy ( ) . into_owned ( ) ) ,
153- display_source : None ,
154- rev : head_rev ,
99+ runtime_source,
100+ display_source : Some ( repo ) ,
101+ rev : snapshot ,
155102 } )
156103 } else {
157104 Ok ( PreparedRepo {
@@ -202,38 +149,45 @@ pub(crate) async fn try_repo(
202149 }
203150
204151 let store = Store :: from_settings ( ) ?;
205- let tmp_dir = TempDir :: with_prefix_in ( "try-repo-" , store. scratch_path ( ) ) ?;
206-
207- let prepared = prepare_repo ( & repo, rev. as_deref ( ) , tmp_dir. path ( ) )
208- . await
209- . context ( "Failed to determine repository and revision" ) ?;
210-
211- let store = Store :: from_path ( tmp_dir. path ( ) ) . init ( ) ?;
212- let repo_config = config:: RemoteRepo :: new (
213- prepared. runtime_source . to_string ( ) ,
214- prepared. rev . clone ( ) ,
215- vec ! [ ] ,
216- ) ;
217- let repo_clone_path = store. clone_repo ( & repo_config, None ) . await ?;
218-
219152 let selectors = Selectors :: load ( & run_args. includes , & run_args. skips , GIT_ROOT . as_ref ( ) ?) ?;
220153
221- let manifest =
222- config:: read_manifest ( & repo_clone_path. join ( prek_consts:: PRE_COMMIT_HOOKS_YAML ) ) ?;
223-
224- let hooks = manifest
225- . hooks
226- . into_iter ( )
227- . filter ( |hook| selectors. matches_hook_id ( & hook. id ) )
228- . map ( |hook| hook. id )
229- . collect :: < Vec < _ > > ( ) ;
154+ let ( _tmp_dir, prepared, hooks, config_str, config_file) = {
155+ let _lock = store. lock_async ( ) . await ?;
156+ // `cache gc` clears the store scratch directory after taking the store lock. Keep the
157+ // active generated config in the system temporary directory so it survives between this
158+ // preparation lock and the lock acquired by `run`.
159+ let tmp_dir = TempDir :: with_prefix ( "try-repo-" ) ?;
160+ let prepared = prepare_repo ( & store, & repo, rev. as_deref ( ) )
161+ . await
162+ . context ( "Failed to determine repository and revision" ) ?;
163+ let repo_config = config:: RemoteRepo :: new (
164+ prepared. runtime_source . to_string ( ) ,
165+ prepared. rev . clone ( ) ,
166+ vec ! [ ] ,
167+ ) ;
168+ let repo_clone_path = store. clone_repo ( & repo_config, None ) . await ?;
169+ let manifest =
170+ config:: read_manifest ( & repo_clone_path. join ( prek_consts:: PRE_COMMIT_HOOKS_YAML ) ) ?;
171+ let hooks = manifest
172+ . hooks
173+ . into_iter ( )
174+ . filter ( |hook| selectors. matches_hook_id ( & hook. id ) )
175+ . map ( |hook| hook. id )
176+ . collect :: < Vec < _ > > ( ) ;
177+
178+ let config_str = render_repo_config_toml ( & prepared. runtime_source , & prepared. rev , & hooks) ;
179+ let config_file = tmp_dir. path ( ) . join ( PREK_TOML ) ;
180+ fs_err:: tokio:: write ( & config_file, & config_str) . await ?;
181+ // Make the new source/checkout visible to GC before releasing the store lock. `run` also
182+ // tracks this path, but a concurrent `cache gc` must not sweep a dirty synthetic commit in
183+ // the interval between preparation and hook initialization.
184+ store. track_configs ( std:: iter:: once ( config_file. as_path ( ) ) ) ?;
185+
186+ ( tmp_dir, prepared, hooks, config_str, config_file)
187+ } ;
230188
231189 // The scratch config needs the resolved source, while the displayed config should preserve
232190 // the user's path so it remains meaningful when copied into their project.
233- let config_str = render_repo_config_toml ( & prepared. runtime_source , & prepared. rev , & hooks) ;
234- let config_file = tmp_dir. path ( ) . join ( PREK_TOML ) ;
235- fs_err:: tokio:: write ( & config_file, & config_str) . await ?;
236-
237191 let display_config_str = match prepared. display_source {
238192 Some ( source) => Cow :: Owned ( render_repo_config_toml ( source, & prepared. rev , & hooks) ) ,
239193 None => Cow :: Borrowed ( config_str. as_str ( ) ) ,
0 commit comments