Skip to content

Commit e77ca5a

Browse files
committed
Reuse remote repository sources
1 parent 61cbcbf commit e77ca5a

13 files changed

Lines changed: 1313 additions & 305 deletions

File tree

crates/prek/src/cli/cache_gc.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::store::{CacheBucket, REPO_MARKER, Store, ToolBucket};
2525
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
2626
enum RemovalKind {
2727
Repos,
28+
RepoSources,
2829
HookEnvs,
2930
Tools,
3031
CacheEntries,
@@ -36,6 +37,7 @@ impl RemovalKind {
3637
if count > 1 {
3738
match self {
3839
RemovalKind::Repos => "repos",
40+
RemovalKind::RepoSources => "repo sources",
3941
RemovalKind::HookEnvs => "hook envs",
4042
RemovalKind::Tools => "tools",
4143
RemovalKind::CacheEntries => "cache entries",
@@ -44,6 +46,7 @@ impl RemovalKind {
4446
} else {
4547
match self {
4648
RemovalKind::Repos => "repo",
49+
RemovalKind::RepoSources => "repo source",
4750
RemovalKind::HookEnvs => "hook env",
4851
RemovalKind::Tools => "tool",
4952
RemovalKind::CacheEntries => "cache entry",
@@ -159,6 +162,7 @@ pub(crate) async fn cache_gc(
159162

160163
let mut kept_configs: FxHashSet<&Path> = FxHashSet::default();
161164
let mut used_repo_keys: FxHashSet<String> = FxHashSet::default();
165+
let mut used_repo_source_keys: FxHashSet<String> = FxHashSet::default();
162166
let mut used_hook_env_dirs: FxHashSet<String> = FxHashSet::default();
163167
let mut used_tools: FxHashSet<ToolBucket> = FxHashSet::default();
164168
let mut used_tool_versions: FxHashMap<ToolBucket, FxHashSet<String>> = FxHashMap::default();
@@ -171,7 +175,7 @@ pub(crate) async fn cache_gc(
171175
let install_cache = InstallCache::new();
172176

173177
for config_path in &tracked_configs {
174-
let config = match load_config(config_path) {
178+
let mut config = match load_config(config_path) {
175179
Ok(config) => {
176180
trace!(path = %config_path.display(), "Found tracked config");
177181
config
@@ -188,6 +192,11 @@ pub(crate) async fn cache_gc(
188192
}
189193
},
190194
};
195+
if let Err(err) = config::resolve_relative_repo_paths(&mut config, config_path) {
196+
warn!(path = %config_path.display(), %err, "Failed to resolve config repo paths, skipping for GC");
197+
kept_configs.insert(config_path);
198+
continue;
199+
}
191200
kept_configs.insert(config_path);
192201

193202
used_env_requirements.extend(hook_env_requirements_from_config(store, &config));
@@ -204,6 +213,7 @@ pub(crate) async fn cache_gc(
204213
if let Some(key) = key {
205214
used_repo_keys.insert(key);
206215
}
216+
used_repo_source_keys.insert(Store::repo_source_key(&remote.repo));
207217
}
208218
}
209219
}
@@ -251,6 +261,15 @@ pub(crate) async fn cache_gc(
251261
verbose,
252262
)?;
253263

264+
// Sweep repo-sources/<hash(repo)>; the hidden lock directory is ignored by the sweeper.
265+
let removed_repo_sources = sweep_dir_by_name(
266+
RemovalKind::RepoSources,
267+
&store.repo_sources_dir(),
268+
&used_repo_source_keys,
269+
dry_run,
270+
verbose,
271+
)?;
272+
254273
// Sweep hooks/<hash>
255274
let removed_hooks = sweep_dir_by_name(
256275
RemovalKind::HookEnvs,
@@ -288,15 +307,20 @@ pub(crate) async fn cache_gc(
288307
verbose,
289308
)?;
290309

291-
// Seep scratch/, as it is only temporary data.
310+
// Sweep scratch/, as it is only temporary data.
292311
if !dry_run {
293312
let _ = fs_err::remove_dir_all(store.scratch_path());
313+
// `Store::init` establishes this directory as an invariant. Recreate it while the global
314+
// store lock is still held so a command that was waiting for GC can immediately create
315+
// clone/download temporaries with its already-initialized `Store`.
316+
fs_err::create_dir_all(store.scratch_path())?;
294317
}
295318
// Keep recent recovery patches, but clear out stale ones that are unlikely to be useful.
296319
let removed_patches = sweep_stale_patch_files(&store.patches_dir(), dry_run, verbose)?;
297320

298321
let mut removed = RemovalSummary::default();
299322
removed += &removed_repos;
323+
removed += &removed_repo_sources;
300324
removed += &removed_hooks;
301325
removed += &removed_tools;
302326
removed += &removed_cache;
@@ -318,6 +342,7 @@ pub(crate) async fn cache_gc(
318342

319343
if verbose {
320344
print_removed_details(printer, verb, &removed_repos)?;
345+
print_removed_details(printer, verb, &removed_repo_sources)?;
321346
print_removed_details(printer, verb, &removed_hooks)?;
322347
print_removed_details(printer, verb, &removed_tools)?;
323348
print_removed_details(printer, verb, &removed_cache)?;

crates/prek/src/cli/try_repo.rs

Lines changed: 49 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
9332
struct PreparedRepo<'a> {
9433
runtime_source: Cow<'a, str>,
9534
display_source: Option<&'a str>,
9635
rev: String,
9736
}
9837

9938
async 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()),

crates/prek/src/cli/update/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,20 @@ pub(crate) async fn update(
386386
filesystem: Option<FilesystemOptions>,
387387
printer: Printer,
388388
) -> Result<ExitStatus> {
389+
// Repository sources are shared with normal hook initialization and try-repo. Keep the
390+
// command-level store invariant while per-source locks below serialize individual fetches.
391+
let _store_lock = store.lock_async().await?;
392+
389393
let workspace_root = Workspace::find_root(config.as_deref(), &CWD)?;
390394
// TODO: support selectors?
391395
let selectors = Selectors::default();
392396
let workspace = Workspace::discover(store, workspace_root, config, Some(&selectors), true)?;
397+
store.track_configs(
398+
workspace
399+
.projects()
400+
.iter()
401+
.map(|project| project.config_file()),
402+
)?;
393403

394404
let tag_filters =
395405
TagFilters::new(include_tag, exclude_tag, repo_include_tag, repo_exclude_tag)?;
@@ -409,7 +419,7 @@ pub(crate) async fn update(
409419
.map(async |repo_source| {
410420
let progress = reporter.on_update_start(repo_source.repo);
411421
let result =
412-
evaluate_repo_source(repo_source, bleeding_edge, freeze, &tag_filters).await;
422+
evaluate_repo_source(store, repo_source, bleeding_edge, freeze, &tag_filters).await;
413423
reporter.on_update_complete(progress);
414424
result
415425
})

0 commit comments

Comments
 (0)