-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Fix git repository cloning to use temp directory first #23601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c61f112
63db502
f0b7042
bc6cdda
c7a2c79
2e062b1
8ce582d
009adfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -480,6 +480,7 @@ pub const Repository = extern struct { | |
| env: DotEnv.Map, | ||
| log: *logger.Log, | ||
| cache_dir: std.fs.Dir, | ||
| temp_dir: std.fs.Dir, | ||
| task_id: Install.Task.Id, | ||
| name: string, | ||
| url: string, | ||
|
|
@@ -511,7 +512,24 @@ pub const Repository = extern struct { | |
| } else |not_found| clone: { | ||
| if (not_found != error.FileNotFound) return not_found; | ||
|
|
||
| const target = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto); | ||
| const time_started_for_verbose_logs: u64 = if (PackageManager.verbose_install) bun.getRoughTickCount().ns() else 0; | ||
|
|
||
| const temp_path_buf = bun.path_buffer_pool.get(); | ||
| defer bun.path_buffer_pool.put(temp_path_buf); | ||
| const tmpname_buf = bun.path_buffer_pool.get(); | ||
| defer bun.path_buffer_pool.put(tmpname_buf); | ||
|
|
||
| // Clone to temp directory first | ||
| const tmpname = try FileSystem.tmpname(folder_name[0..@min(folder_name.len, 32)], tmpname_buf, bun.fastRandom()); | ||
| const temp_target = try bun.getFdPath(.fromStdDir(temp_dir), temp_path_buf); | ||
| const temp_target_full = Path.joinAbsString(temp_target, &.{tmpname}, .auto); | ||
| var moved = false; | ||
| errdefer if (!moved) temp_dir.deleteTree(tmpname) catch {}; | ||
|
|
||
| if (PackageManager.verbose_install) { | ||
| Output.prettyErrorln("[{s}] Cloning git repository (bare) to {s}<r>", .{ name, temp_target_full }); | ||
| Output.flush(); | ||
| } | ||
|
Comment on lines
+529
to
+532
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Use a scoped logger for new verbose logs Switch prettyErrorln/flush to a scoped logger per project guidelines. Apply once near top-level (or per function) and replace calls: +const RepoLog = Output.scoped(.install_repository, .hidden);
...
- Output.prettyErrorln("[{s}] Cloning git repository (bare) to {s}<r>", .{ name, temp_target_full });
- Output.flush();
+ RepoLog("[{s}] Cloning git repository (bare) to {s}", .{ name, temp_target_full });
...
- Output.prettyErrorln("[{s}] Cloned bare repository to cache ({})<r>", .{ name, std.fmt.fmtDuration(elapsed) });
- Output.flush();
+ RepoLog("[{s}] Cloned bare repository to cache ({})", .{ name, std.fmt.fmtDuration(elapsed) });
...
- Output.prettyErrorln("[{s}] Cloning git repository to {s}<r>", .{ name, temp_target_full });
- Output.flush();
+ RepoLog("[{s}] Cloning git repository to {s}", .{ name, temp_target_full });
...
- Output.prettyErrorln("[{s}] Checking out {s}<r>", .{ name, resolved });
- Output.flush();
+ RepoLog("[{s}] Checking out {s}", .{ name, resolved });
...
- Output.prettyErrorln("[{s}] Checked out to cache ({})<r>", .{ name, std.fmt.fmtDuration(elapsed) });
- Output.flush();
+ RepoLog("[{s}] Checked out to cache ({})", .{ name, std.fmt.fmtDuration(elapsed) });As per coding guidelines Also applies to: 587-591, 661-664, 685-688, 745-749 🤖 Prompt for AI Agents |
||
|
|
||
| _ = exec(allocator, env, &[_]string{ | ||
| "git", | ||
|
|
@@ -520,7 +538,7 @@ pub const Repository = extern struct { | |
| "--quiet", | ||
| "--bare", | ||
| url, | ||
| target, | ||
| temp_target_full, | ||
| }) catch |err| { | ||
| if (err == error.RepositoryNotFound or attempt > 1) { | ||
| log.addErrorFmt( | ||
|
|
@@ -534,6 +552,44 @@ pub const Repository = extern struct { | |
| return err; | ||
| }; | ||
|
|
||
| // Move from temp to cache atomically | ||
| if (bun.sys.renameatConcurrently( | ||
| .fromStdDir(temp_dir), | ||
| tmpname, | ||
| .fromStdDir(cache_dir), | ||
| folder_name, | ||
| .{ .move_fallback = true }, | ||
| ).asErr()) |err| { | ||
| // If destination exists, another process likely completed the work | ||
| if (cache_dir.openDirZ(folder_name, .{})) |dir2| { | ||
| // Best-effort cleanup of our temp path | ||
| temp_dir.deleteTree(tmpname) catch {}; | ||
| moved = true; // cancel errdefer; already cleaned up | ||
|
Comment on lines
+566
to
+567
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can't we just let the Then we can get rid of the |
||
| if (PackageManager.verbose_install) { | ||
| Output.prettyErrorln("[{s}] Cache dir already exists (another process completed first)<r>", .{name}); | ||
| Output.flush(); | ||
| } | ||
| break :clone dir2; | ||
| } else |_| {} | ||
| const cache_target = try bun.getFdPath(.fromStdDir(cache_dir), &final_path_buf); | ||
| const dest_full = Path.joinAbsString(cache_target, &.{folder_name}, .auto); | ||
| log.addErrorFmt( | ||
| null, | ||
| logger.Loc.Empty, | ||
| allocator, | ||
| "moving \"{s}\" to cache dir failed: {}\n From: {s}\n To: {s}", | ||
| .{ name, err, temp_target_full, dest_full }, | ||
| ) catch unreachable; | ||
| return error.InstallFailed; | ||
| } | ||
| moved = true; | ||
|
|
||
| if (PackageManager.verbose_install) { | ||
| const elapsed = bun.getRoughTickCount().ns() - time_started_for_verbose_logs; | ||
| Output.prettyErrorln("[{s}] Cloned bare repository to cache ({})<r>", .{ name, std.fmt.fmtDuration(elapsed) }); | ||
| Output.flush(); | ||
| } | ||
|
|
||
| break :clone try cache_dir.openDirZ(folder_name, .{}); | ||
| }; | ||
| } | ||
|
|
@@ -577,6 +633,7 @@ pub const Repository = extern struct { | |
| env: DotEnv.Map, | ||
| log: *logger.Log, | ||
| cache_dir: std.fs.Dir, | ||
| temp_dir: std.fs.Dir, | ||
| repo_dir: std.fs.Dir, | ||
| name: string, | ||
| url: string, | ||
|
|
@@ -588,7 +645,23 @@ pub const Repository = extern struct { | |
| var package_dir = bun.openDir(cache_dir, folder_name) catch |not_found| brk: { | ||
| if (not_found != error.ENOENT) return not_found; | ||
|
|
||
| const target = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto); | ||
| const time_started_for_verbose_logs: u64 = if (PackageManager.verbose_install) bun.getRoughTickCount().ns() else 0; | ||
|
|
||
| const temp_path_buf = bun.path_buffer_pool.get(); | ||
| defer bun.path_buffer_pool.put(temp_path_buf); | ||
| const tmpname_buf = bun.path_buffer_pool.get(); | ||
| defer bun.path_buffer_pool.put(tmpname_buf); | ||
|
|
||
| const tmpname = try FileSystem.tmpname(folder_name[0..@min(folder_name.len, 32)], tmpname_buf, bun.fastRandom()); | ||
| const temp_target = try bun.getFdPath(.fromStdDir(temp_dir), temp_path_buf); | ||
| const temp_target_full = Path.joinAbsString(temp_target, &.{tmpname}, .auto); | ||
| var moved = false; | ||
| errdefer if (!moved) temp_dir.deleteTree(tmpname) catch {}; | ||
|
|
||
| if (PackageManager.verbose_install) { | ||
| Output.prettyErrorln("[{s}] Cloning git repository to {s}<r>", .{ name, temp_target_full }); | ||
| Output.flush(); | ||
| } | ||
|
|
||
| _ = exec(allocator, env, &[_]string{ | ||
| "git", | ||
|
|
@@ -597,7 +670,7 @@ pub const Repository = extern struct { | |
| "--quiet", | ||
| "--no-checkout", | ||
| try bun.getFdPath(.fromStdDir(repo_dir), &final_path_buf), | ||
| target, | ||
| temp_target_full, | ||
| }) catch |err| { | ||
| log.addErrorFmt( | ||
| null, | ||
|
|
@@ -609,9 +682,12 @@ pub const Repository = extern struct { | |
| return err; | ||
| }; | ||
|
|
||
| const folder = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto); | ||
| if (PackageManager.verbose_install) { | ||
| Output.prettyErrorln("[{s}] Checking out {s}<r>", .{ name, resolved }); | ||
| Output.flush(); | ||
| } | ||
|
|
||
| _ = exec(allocator, env, &[_]string{ "git", "-C", folder, "checkout", "--quiet", resolved }) catch |err| { | ||
| _ = exec(allocator, env, &[_]string{ "git", "-C", temp_target_full, "checkout", "--quiet", resolved }) catch |err| { | ||
| log.addErrorFmt( | ||
| null, | ||
| logger.Loc.Empty, | ||
|
|
@@ -621,18 +697,58 @@ pub const Repository = extern struct { | |
| ) catch unreachable; | ||
| return err; | ||
| }; | ||
| var dir = try bun.openDir(cache_dir, folder_name); | ||
| dir.deleteTree(".git") catch {}; | ||
|
|
||
| var temp_pkg_dir = try bun.openDir(temp_dir, tmpname); | ||
| temp_pkg_dir.deleteTree(".git") catch {}; | ||
|
|
||
| if (resolved.len > 0) insert_tag: { | ||
| const git_tag = dir.createFileZ(".bun-tag", .{ .truncate = true }) catch break :insert_tag; | ||
| const git_tag = temp_pkg_dir.createFileZ(".bun-tag", .{ .truncate = true }) catch break :insert_tag; | ||
| defer git_tag.close(); | ||
| git_tag.writeAll(resolved) catch { | ||
| dir.deleteFileZ(".bun-tag") catch {}; | ||
| temp_pkg_dir.deleteFileZ(".bun-tag") catch {}; | ||
| }; | ||
| } | ||
| temp_pkg_dir.close(); | ||
|
|
||
|
Comment on lines
+701
to
+712
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Remove .git and tag the worktree: LGTM (optional verbose on failure) Silently ignoring .git removal is acceptable; consider emitting a verbose-only note if removal fails. |
||
| // Move from temp to cache atomically | ||
| if (bun.sys.renameatConcurrently( | ||
| .fromStdDir(temp_dir), | ||
| tmpname, | ||
| .fromStdDir(cache_dir), | ||
| folder_name, | ||
| .{ .move_fallback = true }, | ||
| ).asErr()) |err| { | ||
| // If destination exists, another process likely completed the work | ||
| if (bun.openDir(cache_dir, folder_name)) |_| { | ||
| // Best-effort cleanup of our temp path | ||
| temp_dir.deleteTree(tmpname) catch {}; | ||
| moved = true; // cancel errdefer; already cleaned up | ||
| if (PackageManager.verbose_install) { | ||
| Output.prettyErrorln("[{s}] Cache dir already exists (another process completed first)<r>", .{name}); | ||
| Output.flush(); | ||
| } | ||
| break :brk try bun.openDir(cache_dir, folder_name); | ||
| } else |_| {} | ||
| const cache_target = try bun.getFdPath(.fromStdDir(cache_dir), &final_path_buf); | ||
| const dest_full = Path.joinAbsString(cache_target, &.{folder_name}, .auto); | ||
| log.addErrorFmt( | ||
| null, | ||
| logger.Loc.Empty, | ||
| allocator, | ||
| "moving \"{s}\" to cache dir failed: {}\n From: {s}\n To: {s}", | ||
| .{ name, err, temp_target_full, dest_full }, | ||
| ) catch unreachable; | ||
| return error.InstallFailed; | ||
| } | ||
| moved = true; | ||
|
|
||
| if (PackageManager.verbose_install) { | ||
| const elapsed = bun.getRoughTickCount().ns() - time_started_for_verbose_logs; | ||
| Output.prettyErrorln("[{s}] Checked out to cache ({})<r>", .{ name, std.fmt.fmtDuration(elapsed) }); | ||
| Output.flush(); | ||
| } | ||
|
|
||
| break :brk dir; | ||
| break :brk try bun.openDir(cache_dir, folder_name); | ||
| }; | ||
| defer package_dir.close(); | ||
|
|
||
|
|
@@ -695,6 +811,7 @@ const PackageManager = Install.PackageManager; | |
|
|
||
| const bun = @import("bun"); | ||
| const OOM = bun.OOM; | ||
| const Output = bun.Output; | ||
| const Path = bun.path; | ||
| const logger = bun.logger; | ||
| const strings = bun.strings; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Signature change: ensure all call sites pass temp_dir
Both download() and checkout() now require temp_dir. Compile passed, but please double-check all call sites.
Run:
Also applies to: 633-634
🏁 Script executed:
Length of output: 5006
🏁 Script executed:
Length of output: 2133
Add missing
task_idargument to Repository.download and checkout callsThe invocations in src/install/PackageManagerTask.zig (lines 160, 188, 213) pass allocator, env, log, cacheDir, and tempDir but omit the new
task_idparameter. Append the appropriateInstall.Task.Idto each call.🤖 Prompt for AI Agents