threadlocal var final_path_buf: bun.PathBuffer = undefined; threadlocal var ssh_path_buf: bun.PathBuffer = undefined; threadlocal var folder_name_buf: bun.PathBuffer = undefined; threadlocal var json_path_buf: bun.PathBuffer = undefined; const SloppyGlobalGitConfig = struct { has_askpass: bool = false, has_ssh_command: bool = false, var holder: SloppyGlobalGitConfig = .{}; var load_and_parse_once = std.once(loadAndParse); pub fn get() SloppyGlobalGitConfig { load_and_parse_once.call(); return holder; } pub fn loadAndParse() void { const home_dir = bun.env_var.HOME.get() orelse return; var config_file_path_buf: bun.PathBuffer = undefined; const config_file_path = bun.path.joinAbsStringBufZ(home_dir, &config_file_path_buf, &.{".gitconfig"}, .auto); var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); const allocator = stack_fallback.get(); const source = File.toSource(config_file_path, allocator, .{ .convert_bom = true }).unwrap() catch { return; }; defer allocator.free(source.contents); var remaining = bun.strings.split(source.contents, "\n"); var found_askpass = false; var found_ssh_command = false; var @"[core]" = false; while (remaining.next()) |line_| { if (found_askpass and found_ssh_command) break; const line = strings.trim(line_, "\t \r"); if (line.len == 0) continue; // skip comments if (line[0] == '#') continue; if (line[0] == '[') { if (strings.indexOfChar(line, ']')) |end_bracket| { if (strings.eqlComptime(line[0 .. end_bracket + 1], "[core]")) { @"[core]" = true; continue; } } @"[core]" = false; continue; } if (@"[core]") { if (!found_askpass) { if (line.len > "askpass".len and strings.eqlCaseInsensitiveASCIIIgnoreLength(line[0.."askpass".len], "askpass") and switch (line["askpass".len]) { ' ', '\t', '=' => true, else => false, }) { found_askpass = true; continue; } } if (!found_ssh_command) { if (line.len > "sshCommand".len and strings.eqlCaseInsensitiveASCIIIgnoreLength(line[0.."sshCommand".len], "sshCommand") and switch (line["sshCommand".len]) { ' ', '\t', '=' => true, else => false, }) { found_ssh_command = true; } } } else { if (!found_askpass) { if (line.len > "core.askpass".len and strings.eqlCaseInsensitiveASCIIIgnoreLength(line[0.."core.askpass".len], "core.askpass") and switch (line["core.askpass".len]) { ' ', '\t', '=' => true, else => false, }) { found_askpass = true; continue; } } if (!found_ssh_command) { if (line.len > "core.sshCommand".len and strings.eqlCaseInsensitiveASCIIIgnoreLength(line[0.."core.sshCommand".len], "core.sshCommand") and switch (line["core.sshCommand".len]) { ' ', '\t', '=' => true, else => false, }) { found_ssh_command = true; } } } } holder = .{ .has_askpass = found_askpass, .has_ssh_command = found_ssh_command, }; } }; pub const Repository = extern struct { owner: String = .{}, repo: String = .{}, committish: String = .{}, resolved: String = .{}, package_name: String = .{}, pub var shared_env: struct { env: ?DotEnv.Map = null, pub fn get(this: *@This(), allocator: std.mem.Allocator, other: *DotEnv.Loader) DotEnv.Map { return this.env orelse brk: { // Note: currently if the user sets this to some value that causes // a prompt for a password, the stdout of the prompt will be masked // by further output of the rest of the install process. // A value can still be entered, but we need to find a workaround // so the user can see what is being prompted. By default the settings // below will cause no prompt and throw instead. var cloned = bun.handleOom(other.map.cloneWithAllocator(allocator)); if (cloned.get("GIT_ASKPASS") == null) { const config = SloppyGlobalGitConfig.get(); if (!config.has_askpass) { bun.handleOom(cloned.put("GIT_ASKPASS", "echo")); } } if (cloned.get("GIT_SSH_COMMAND") == null) { const config = SloppyGlobalGitConfig.get(); if (!config.has_ssh_command) { bun.handleOom(cloned.put("GIT_SSH_COMMAND", "ssh -oStrictHostKeyChecking=accept-new")); } } this.env = cloned; break :brk this.env.?; }; } } = .{}; pub const Hosts = bun.ComptimeStringMap(string, .{ .{ "bitbucket", ".org" }, .{ "github", ".com" }, .{ "gitlab", ".com" }, }); pub fn parseAppendGit(input: string, buf: *String.Buf) OOM!Repository { var remain = input; if (strings.hasPrefixComptime(remain, "git+")) { remain = remain["git+".len..]; } if (strings.lastIndexOfChar(remain, '#')) |hash| { return .{ .repo = try buf.append(remain[0..hash]), .committish = try buf.append(remain[hash + 1 ..]), }; } return .{ .repo = try buf.append(remain), }; } pub fn parseAppendGithub(input: string, buf: *String.Buf) OOM!Repository { var remain = input; if (strings.hasPrefixComptime(remain, "github:")) { remain = remain["github:".len..]; } var hash: usize = 0; var slash: usize = 0; for (remain, 0..) |c, i| { switch (c) { '/' => slash = i, '#' => hash = i, else => {}, } } const repo = if (hash == 0) remain[slash + 1 ..] else remain[slash + 1 .. hash]; var result: Repository = .{ .owner = try buf.append(remain[0..slash]), .repo = try buf.append(repo), }; if (hash != 0) { result.committish = try buf.append(remain[hash + 1 ..]); } return result; } pub fn createDependencyNameFromVersionLiteral( allocator: std.mem.Allocator, repository: *const Repository, lockfile: *Install.Lockfile, dep_id: Install.DependencyID, ) []u8 { const buf = lockfile.buffers.string_bytes.items; const dep = lockfile.buffers.dependencies.items[dep_id]; const repo_name = repository.repo; const repo_name_str = lockfile.str(&repo_name); const name = brk: { var remain = repo_name_str; if (strings.indexOfChar(remain, '#')) |hash_index| { remain = remain[0..hash_index]; } if (remain.len == 0) break :brk remain; if (strings.lastIndexOfChar(remain, '/')) |slash_index| { remain = remain[slash_index + 1 ..]; } break :brk remain; }; if (name.len == 0) { const version_literal = dep.version.literal.slice(buf); const name_buf = bun.handleOom(allocator.alloc(u8, bun.sha.EVP.SHA1.digest)); var sha1 = bun.sha.SHA1.init(); defer sha1.deinit(); sha1.update(version_literal); sha1.final(name_buf[0..bun.sha.SHA1.digest]); return name_buf[0..bun.sha.SHA1.digest]; } return bun.handleOom(allocator.dupe(u8, name)); } pub fn order(lhs: *const Repository, rhs: *const Repository, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order { const owner_order = lhs.owner.order(&rhs.owner, lhs_buf, rhs_buf); if (owner_order != .eq) return owner_order; const repo_order = lhs.repo.order(&rhs.repo, lhs_buf, rhs_buf); if (repo_order != .eq) return repo_order; return lhs.committish.order(&rhs.committish, lhs_buf, rhs_buf); } pub fn count(this: *const Repository, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) void { builder.count(this.owner.slice(buf)); builder.count(this.repo.slice(buf)); builder.count(this.committish.slice(buf)); builder.count(this.resolved.slice(buf)); builder.count(this.package_name.slice(buf)); } pub fn clone(this: *const Repository, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) Repository { return .{ .owner = builder.append(String, this.owner.slice(buf)), .repo = builder.append(String, this.repo.slice(buf)), .committish = builder.append(String, this.committish.slice(buf)), .resolved = builder.append(String, this.resolved.slice(buf)), .package_name = builder.append(String, this.package_name.slice(buf)), }; } pub fn eql(lhs: *const Repository, rhs: *const Repository, lhs_buf: []const u8, rhs_buf: []const u8) bool { if (!lhs.owner.eql(rhs.owner, lhs_buf, rhs_buf)) return false; if (!lhs.repo.eql(rhs.repo, lhs_buf, rhs_buf)) return false; if (lhs.resolved.isEmpty() or rhs.resolved.isEmpty()) return lhs.committish.eql(rhs.committish, lhs_buf, rhs_buf); return lhs.resolved.eql(rhs.resolved, lhs_buf, rhs_buf); } pub fn formatAs(this: *const Repository, label: string, buf: []const u8, writer: *std.Io.Writer) std.Io.Writer.Error!void { const formatter = Formatter{ .label = label, .repository = this, .buf = buf }; return try formatter.format(writer); } pub fn fmtStorePath(this: *const Repository, label: string, string_buf: string) StorePathFormatter { return .{ .repo = this, .label = label, .string_buf = string_buf, }; } pub const StorePathFormatter = struct { repo: *const Repository, label: string, string_buf: string, pub fn format(this: StorePathFormatter, writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print("{f}", .{Install.fmtStorePath(this.label)}); if (!this.repo.owner.isEmpty()) { try writer.print("{f}", .{this.repo.owner.fmtStorePath(this.string_buf)}); // try writer.writeByte(if (this.opts.replace_slashes) '+' else '/'); try writer.writeByte('+'); } else if (Dependency.isSCPLikePath(this.repo.repo.slice(this.string_buf))) { // try writer.print("ssh:{s}", .{if (this.opts.replace_slashes) "++" else "//"}); try writer.writeAll("ssh++"); } try writer.print("{f}", .{this.repo.repo.fmtStorePath(this.string_buf)}); if (!this.repo.resolved.isEmpty()) { try writer.writeByte('+'); // this would be '#' but it's not valid on windows var resolved = this.repo.resolved.slice(this.string_buf); if (strings.lastIndexOfChar(resolved, '-')) |i| { resolved = resolved[i + 1 ..]; } try writer.print("{f}", .{Install.fmtStorePath(resolved)}); } else if (!this.repo.committish.isEmpty()) { try writer.writeByte('+'); // this would be '#' but it's not valid on windows try writer.print("{f}", .{this.repo.committish.fmtStorePath(this.string_buf)}); } } }; pub fn fmt(this: *const Repository, label: string, buf: []const u8) Formatter { return .{ .repository = this, .buf = buf, .label = label, }; } pub const Formatter = struct { label: []const u8 = "", buf: []const u8, repository: *const Repository, pub fn format(formatter: Formatter, writer: *std.Io.Writer) std.Io.Writer.Error!void { if (comptime Environment.allow_assert) bun.assert(formatter.label.len > 0); try writer.writeAll(formatter.label); const repo = formatter.repository.repo.slice(formatter.buf); if (!formatter.repository.owner.isEmpty()) { try writer.writeAll(formatter.repository.owner.slice(formatter.buf)); try writer.writeAll("/"); } else if (Dependency.isSCPLikePath(repo)) { try writer.writeAll("ssh://"); } try writer.writeAll(repo); if (!formatter.repository.resolved.isEmpty()) { try writer.writeAll("#"); var resolved = formatter.repository.resolved.slice(formatter.buf); if (strings.lastIndexOfChar(resolved, '-')) |i| { resolved = resolved[i + 1 ..]; } try writer.writeAll(resolved); } else if (!formatter.repository.committish.isEmpty()) { try writer.writeAll("#"); try writer.writeAll(formatter.repository.committish.slice(formatter.buf)); } } }; fn exec( allocator: std.mem.Allocator, _env: DotEnv.Map, argv: []const string, ) !string { var env = _env; var std_map = try env.stdEnvMap(allocator); defer std_map.deinit(); const result = if (comptime Environment.isWindows) try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .env_map = std_map.get(), }) else try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .env_map = std_map.get(), }); switch (result.term) { .Exited => |sig| if (sig == 0) return result.stdout else if ( // remote: The page could not be found <-- for non git // remote: Repository not found. <-- for git // remote: fatal repository '' does not exist <-- for git (strings.containsComptime(result.stderr, "remote:") and strings.containsComptime(result.stderr, "not") and strings.containsComptime(result.stderr, "found")) or strings.containsComptime(result.stderr, "does not exist")) { return error.RepositoryNotFound; }, else => {}, } return error.InstallFailed; } pub fn trySSH(url: string) ?string { // Do not cast explicit http(s) URLs to SSH if (strings.hasPrefixComptime(url, "http")) { return null; } if (strings.hasPrefixComptime(url, "git@")) { return url; } if (strings.hasPrefixComptime(url, "ssh://")) { // TODO(markovejnovic): This is a stop-gap. One of the problems with the implementation // here is that we should integrate hosted_git_info more thoroughly into the codebase // to avoid the allocation and copy here. For now, the thread-local buffer is a good // enough solution to avoid having to handle init/deinit. // Fix malformed ssh:// URLs with colons using hosted_git_info.correctUrl // ssh://git@github.com:user/repo -> ssh://git@github.com/user/repo var pair = hosted_git_info.UrlProtocolPair{ .url = .{ .unmanaged = url }, .protocol = .{ .well_formed = .git_plus_ssh }, }; var corrected = hosted_git_info.correctUrl(&pair, bun.default_allocator) catch { return url; // If correction fails, return original }; defer corrected.deinit(); // Copy corrected URL to thread-local buffer const corrected_str = corrected.urlSlice(); const result = ssh_path_buf[0..corrected_str.len]; bun.copy(u8, result, corrected_str); return result; } if (Dependency.isSCPLikePath(url)) { ssh_path_buf[0.."ssh://git@".len].* = "ssh://git@".*; var rest = ssh_path_buf["ssh://git@".len..]; const colon_index = strings.indexOfChar(url, ':'); if (colon_index) |colon| { // make sure known hosts have `.com` or `.org` if (Hosts.get(url[0..colon])) |tld| { bun.copy(u8, rest, url[0..colon]); bun.copy(u8, rest[colon..], tld); rest[colon + tld.len] = '/'; bun.copy(u8, rest[colon + tld.len + 1 ..], url[colon + 1 ..]); const out = ssh_path_buf[0 .. url.len + "ssh://git@".len + tld.len]; return out; } } bun.copy(u8, rest, url); if (colon_index) |colon| rest[colon] = '/'; const final = ssh_path_buf[0 .. url.len + "ssh://".len]; return final; } return null; } pub fn tryHTTPS(url: string) ?string { if (strings.hasPrefixComptime(url, "http")) { return url; } if (strings.hasPrefixComptime(url, "ssh://")) { final_path_buf[0.."https".len].* = "https".*; bun.copy(u8, final_path_buf["https".len..], url["ssh".len..]); const out = final_path_buf[0 .. url.len - "ssh".len + "https".len]; return out; } if (Dependency.isSCPLikePath(url)) { final_path_buf[0.."https://".len].* = "https://".*; var rest = final_path_buf["https://".len..]; const colon_index = strings.indexOfChar(url, ':'); if (colon_index) |colon| { // make sure known hosts have `.com` or `.org` if (Hosts.get(url[0..colon])) |tld| { bun.copy(u8, rest, url[0..colon]); bun.copy(u8, rest[colon..], tld); rest[colon + tld.len] = '/'; bun.copy(u8, rest[colon + tld.len + 1 ..], url[colon + 1 ..]); const out = final_path_buf[0 .. url.len + "https://".len + tld.len]; return out; } } bun.copy(u8, rest, url); if (colon_index) |colon| rest[colon] = '/'; return final_path_buf[0 .. url.len + "https://".len]; } return null; } pub fn download( allocator: std.mem.Allocator, env: DotEnv.Map, log: *logger.Log, cache_dir: std.fs.Dir, task_id: Install.Task.Id, name: string, url: string, attempt: u8, ) !std.fs.Dir { bun.analytics.Features.git_dependencies += 1; const folder_name = try std.fmt.bufPrintZ(&folder_name_buf, "{f}.git", .{ bun.fmt.hexIntLower(task_id.get()), }); return if (cache_dir.openDirZ(folder_name, .{})) |dir| fetch: { const path = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto); _ = exec( allocator, env, &[_]string{ "git", "-C", path, "fetch", "--quiet" }, ) catch |err| { log.addErrorFmt( null, logger.Loc.Empty, allocator, "\"git fetch\" for \"{s}\" failed", .{name}, ) catch unreachable; return err; }; break :fetch dir; } else |not_found| clone: { if (not_found != error.FileNotFound) return not_found; const target = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto); _ = exec(allocator, env, &[_]string{ "git", "clone", "-c", "core.longpaths=true", "--quiet", "--bare", url, target, }) catch |err| { if (err == error.RepositoryNotFound or attempt > 1) { log.addErrorFmt( null, logger.Loc.Empty, allocator, "\"git clone\" for \"{s}\" failed", .{name}, ) catch unreachable; } return err; }; break :clone try cache_dir.openDirZ(folder_name, .{}); }; } pub fn findCommit( allocator: std.mem.Allocator, env: *DotEnv.Loader, log: *logger.Log, repo_dir: std.fs.Dir, name: string, committish: string, task_id: Install.Task.Id, ) !string { const path = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{try std.fmt.bufPrint(&folder_name_buf, "{f}.git", .{ bun.fmt.hexIntLower(task_id.get()), })}, .auto); _ = repo_dir; return std.mem.trim(u8, exec( allocator, shared_env.get(allocator, env), if (committish.len > 0) &[_]string{ "git", "-C", path, "log", "--format=%H", "-1", committish } else &[_]string{ "git", "-C", path, "log", "--format=%H", "-1" }, ) catch |err| { log.addErrorFmt( null, logger.Loc.Empty, allocator, "no commit matching \"{s}\" found for \"{s}\" (but repository exists)", .{ committish, name }, ) catch unreachable; return err; }, " \t\r\n"); } pub fn checkout( allocator: std.mem.Allocator, env: DotEnv.Map, log: *logger.Log, cache_dir: std.fs.Dir, repo_dir: std.fs.Dir, name: string, url: string, resolved: string, ) !ExtractData { bun.analytics.Features.git_dependencies += 1; const folder_name = PackageManager.cachedGitFolderNamePrint(&folder_name_buf, resolved, null); 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); _ = exec(allocator, env, &[_]string{ "git", "clone", "-c", "core.longpaths=true", "--quiet", "--no-checkout", try bun.getFdPath(.fromStdDir(repo_dir), &final_path_buf), target, }) catch |err| { log.addErrorFmt( null, logger.Loc.Empty, allocator, "\"git clone\" for \"{s}\" failed", .{name}, ) catch unreachable; return err; }; const folder = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto); _ = exec(allocator, env, &[_]string{ "git", "-C", folder, "checkout", "--quiet", resolved }) catch |err| { log.addErrorFmt( null, logger.Loc.Empty, allocator, "\"git checkout\" for \"{s}\" failed", .{name}, ) catch unreachable; return err; }; var dir = try bun.openDir(cache_dir, folder_name); dir.deleteTree(".git") catch {}; if (resolved.len > 0) insert_tag: { const git_tag = dir.createFileZ(".bun-tag", .{ .truncate = true }) catch break :insert_tag; defer git_tag.close(); git_tag.writeAll(resolved) catch { dir.deleteFileZ(".bun-tag") catch {}; }; } break :brk dir; }; defer package_dir.close(); const json_file, const json_buf = bun.sys.File.readFileFrom(package_dir, "package.json", allocator).unwrap() catch |err| { if (err == error.ENOENT) { // allow git dependencies without package.json return .{ .url = url, .resolved = resolved, }; } log.addErrorFmt( null, logger.Loc.Empty, allocator, "\"package.json\" for \"{s}\" failed to open: {s}", .{ name, @errorName(err) }, ) catch unreachable; return error.InstallFailed; }; defer json_file.close(); const json_path = json_file.getPath( &json_path_buf, ).unwrap() catch |err| { log.addErrorFmt( null, logger.Loc.Empty, allocator, "\"package.json\" for \"{s}\" failed to resolve: {s}", .{ name, @errorName(err) }, ) catch unreachable; return error.InstallFailed; }; const ret_json_path = try FileSystem.instance.dirname_store.append(@TypeOf(json_path), json_path); return .{ .url = url, .resolved = resolved, .json = .{ .path = ret_json_path, .buf = json_buf, }, }; } }; const string = []const u8; const Dependency = @import("./dependency.zig"); const DotEnv = @import("../env_loader.zig"); const Environment = @import("../env.zig"); const hosted_git_info = @import("./hosted_git_info.zig"); const std = @import("std"); const FileSystem = @import("../fs.zig").FileSystem; const Install = @import("./install.zig"); const ExtractData = Install.ExtractData; const PackageManager = Install.PackageManager; const bun = @import("bun"); const OOM = bun.OOM; const Path = bun.path; const logger = bun.logger; const strings = bun.strings; const File = bun.sys.File; const Semver = bun.Semver; const String = Semver.String;