diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index ce4bbd8f51..6c90ec612e 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -624,6 +624,22 @@ extern "C" BunString URL__getHrefJoin(BunString* baseStr, BunString* relativeStr return Bun::toStringRef(url.string()); } +extern "C" BunString URL__hash(WTF::URL* url) +{ + const auto& fragment = url->fragmentIdentifier().isEmpty() + ? emptyString() + : url->fragmentIdentifierWithLeadingNumberSign().toStringWithoutCopying(); + return Bun::toStringRef(fragment); +} + +extern "C" BunString URL__fragmentIdentifier(WTF::URL* url) +{ + const auto& fragment = url->fragmentIdentifier().isEmpty() + ? emptyString() + : url->fragmentIdentifier().toStringWithoutCopying(); + return Bun::toStringRef(fragment); +} + extern "C" WTF::URL* URL__fromString(BunString* input) { auto&& str = input->toWTFString(); diff --git a/src/bun.js/bindings/URL.zig b/src/bun.js/bindings/URL.zig index 2d75fe6301..42691e2646 100644 --- a/src/bun.js/bindings/URL.zig +++ b/src/bun.js/bindings/URL.zig @@ -16,6 +16,20 @@ pub const URL = opaque { extern fn URL__getFileURLString(*String) String; extern fn URL__getHrefJoin(*String, *String) String; extern fn URL__pathFromFileURL(*String) String; + extern fn URL__hash(*URL) String; + extern fn URL__fragmentIdentifier(*URL) String; + + /// Includes the leading '#'. + pub fn hash(url: *URL) String { + jsc.markBinding(@src()); + return URL__hash(url); + } + + /// Exactly the same as hash, excluding the leading '#'. + pub fn fragmentIdentifier(url: *URL) String { + jsc.markBinding(@src()); + return URL__fragmentIdentifier(url); + } pub fn hrefFromString(str: bun.String) String { jsc.markBinding(@src()); diff --git a/src/comptime_string_map.zig b/src/comptime_string_map.zig index 4d65cf870f..771e70782c 100644 --- a/src/comptime_string_map.zig +++ b/src/comptime_string_map.zig @@ -299,6 +299,17 @@ pub fn ComptimeStringMapWithKeyType(comptime KeyType: type, comptime V: type, co return null; } + + /// Lookup the first-defined string key for a given value. + /// + /// Linear search. + pub fn getKey(value: V) ?[]const KeyType { + inline for (kvs) |kv| { + if (kv.value == value) return kv.key; + } + + return null; + } }; } diff --git a/src/install/dependency.zig b/src/install/dependency.zig index e92f49f051..6e3ce8e255 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -173,45 +173,6 @@ pub inline fn isSCPLikePath(dependency: string) bool { return false; } -/// `isGitHubShorthand` from npm -/// https://github.com/npm/cli/blob/22731831e22011e32fa0ca12178e242c2ee2b33d/node_modules/hosted-git-info/lib/from-url.js#L6 -pub inline fn isGitHubRepoPath(dependency: string) bool { - // Shortest valid expression: u/r - if (dependency.len < 3) return false; - - var hash_index: usize = 0; - - // the branch could have slashes - // - oven-sh/bun#brach/name - var first_slash_index: usize = 0; - - for (dependency, 0..) |c, i| { - switch (c) { - '/' => { - if (i == 0) return false; - if (first_slash_index == 0) { - first_slash_index = i; - } - }, - '#' => { - if (i == 0) return false; - if (hash_index > 0) return false; - if (first_slash_index == 0) return false; - hash_index = i; - }, - // Not allowed in username - '.', '_' => { - if (first_slash_index == 0) return false; - }, - // Must be alphanumeric - '-', 'a'...'z', 'A'...'Z', '0'...'9' => {}, - else => return false, - } - } - - return hash_index != dependency.len - 1 and first_slash_index > 0 and first_slash_index != dependency.len - 1; -} - /// Github allows for the following format of URL: /// https://github.com///tarball/ /// This is a legacy (but still supported) method of retrieving a tarball of an @@ -533,6 +494,10 @@ pub const Version = struct { return .folder; } + // Allocator necessary for slow paths. + var stackFallback = std.heap.stackFallback(1024, bun.default_allocator); + const allocator = stackFallback.get(); + switch (dependency[0]) { // =1 // >1.2 @@ -599,11 +564,20 @@ pub const Version = struct { if (url.len > 2) { switch (url[0]) { ':' => { + // TODO(markovejnovic): This check for testing whether the URL + // is a Git URL shall be moved to npm_package_arg.zig when that + // is implemented. if (strings.hasPrefixComptime(url, "://")) { url = url["://".len..]; if (strings.hasPrefixComptime(url, "github.com/")) { - if (isGitHubRepoPath(url["github.com/".len..])) return .github; + if (hosted_git_info.isGitHubShorthand(url["github.com/".len..])) return .github; } + + if (hosted_git_info.HostedGitInfo.fromUrl(allocator, dependency) catch null) |info| { + defer info.deinit(); + return hgiToTag(info); + } + return .git; } }, @@ -633,15 +607,21 @@ pub const Version = struct { else => false, }) { if (strings.hasPrefixComptime(url, "github.com/")) { - if (isGitHubRepoPath(url["github.com/".len..])) return .github; + if (hosted_git_info.isGitHubShorthand(url["github.com/".len..])) return .github; } + + if (hosted_git_info.HostedGitInfo.fromUrl(allocator, dependency) catch null) |info| { + defer info.deinit(); + return hgiToTag(info); + } + return .git; } } }, 'h' => { if (strings.hasPrefixComptime(url, "hub:")) { - if (isGitHubRepoPath(url["hub:".len..])) return .github; + if (hosted_git_info.isGitHubShorthand(url["hub:".len..])) return .github; } }, else => {}, @@ -673,11 +653,12 @@ pub const Version = struct { if (strings.hasPrefixComptime(url, "github.com/")) { const path = url["github.com/".len..]; if (isGitHubTarballPath(path)) return .tarball; - if (isGitHubRepoPath(path)) return .github; + if (hosted_git_info.isGitHubShorthand(path)) return .github; } - if (strings.indexOfChar(url, '.')) |dot| { - if (Repository.Hosts.has(url[0..dot])) return .git; + if (hosted_git_info.HostedGitInfo.fromUrl(allocator, dependency) catch null) |info| { + defer info.deinit(); + return hgiToTag(info); } return .tarball; @@ -698,9 +679,11 @@ pub const Version = struct { url = url["git@".len..]; } - if (strings.indexOfChar(url, '.')) |dot| { - if (Repository.Hosts.has(url[0..dot])) return .git; + if (hosted_git_info.HostedGitInfo.fromUrl(allocator, dependency) catch null) |info| { + defer info.deinit(); + return hgiToTag(info); } + return .git; } } }, @@ -732,7 +715,7 @@ pub const Version = struct { // virt@example.com:repo.git 'v' => { if (isTarball(dependency)) return .tarball; - if (isGitHubRepoPath(dependency)) return .github; + if (hosted_git_info.isGitHubShorthand(dependency)) return .github; if (isSCPLikePath(dependency)) return .git; if (dependency.len == 1) return .dist_tag; return switch (dependency[1]) { @@ -765,11 +748,20 @@ pub const Version = struct { // foo.tgz // bar.tar.gz if (isTarball(dependency)) return .tarball; + // user/repo // user/repo#main - if (isGitHubRepoPath(dependency)) return .github; + if (hosted_git_info.isGitHubShorthand(dependency)) return .github; + // git@example.com:path/to/repo.git - if (isSCPLikePath(dependency)) return .git; + if (isSCPLikePath(dependency)) { + if (hosted_git_info.HostedGitInfo.fromUrl(allocator, dependency) catch null) |info| { + defer info.deinit(); + return hgiToTag(info); + } + return .git; + } + // beta if (!strings.containsChar(dependency, '|')) { @@ -785,7 +777,14 @@ pub const Version = struct { return .js_undefined; } - const tag = try Tag.fromJS(globalObject, arguments[0]) orelse return .js_undefined; + // Convert JSValue to string slice + const dependency_str = try arguments[0].toBunString(globalObject); + defer dependency_str.deref(); + var as_utf8 = dependency_str.toUTF8(bun.default_allocator); + defer as_utf8.deinit(); + + // Infer the tag from the dependency string + const tag = Tag.infer(as_utf8.slice()); var str = bun.String.init(@tagName(tag)); return str.transferToJS(globalObject); } @@ -1041,70 +1040,51 @@ pub fn parseWithTag( }; }, .github => { - var from_url = false; - var input = dependency; - if (strings.hasPrefixComptime(input, "github:")) { - input = input["github:".len..]; - } else if (strings.hasPrefixComptime(input, "git://github.com/")) { - input = input["git://github.com/".len..]; - from_url = true; - } else { - if (strings.hasPrefixComptime(input, "git+")) { - input = input["git+".len..]; - } - if (strings.hasPrefixComptime(input, "http")) { - var url = input["http".len..]; - if (url.len > 2) { - switch (url[0]) { - ':' => { - if (strings.hasPrefixComptime(url, "://")) { - url = url["://".len..]; - } - }, - 's' => { - if (strings.hasPrefixComptime(url, "s://")) { - url = url["s://".len..]; - } - }, - else => {}, - } - if (strings.hasPrefixComptime(url, "github.com/")) { - input = url["github.com/".len..]; - from_url = true; - } - } - } - } + const info = bun.handleOom( + hosted_git_info.HostedGitInfo.fromUrl(allocator, dependency), + ) catch { + return null; + } orelse { + return null; + }; + defer info.deinit(); - if (comptime Environment.allow_assert) bun.assert(isGitHubRepoPath(input)); + // Now we have parsed info, we need to find these substrings in the original dependency + // to create String objects that point to the original buffer + const owner_str = info.user orelse ""; + const repo_str = info.project; + const committish_str = info.committish orelse ""; - var hash_index: usize = 0; - var slash_index: usize = 0; - for (input, 0..) |c, i| { - switch (c) { - '/' => { - slash_index = i; - }, - '#' => { - hash_index = i; - break; - }, - else => {}, - } - } + // Find owner in dependency string + const owner_idx = strings.indexOf(dependency, owner_str); + const owner = if (owner_idx) |idx| + sliced.sub(dependency[idx .. idx + owner_str.len]).value() + else + String.from(""); - var repo = if (hash_index == 0) input[slash_index + 1 ..] else input[slash_index + 1 .. hash_index]; - if (from_url and strings.endsWithComptime(repo, ".git")) { - repo = repo[0 .. repo.len - ".git".len]; - } + // Find repo in dependency string + const repo_idx = strings.indexOf(dependency, repo_str); + const repo = if (repo_idx) |idx| + sliced.sub(dependency[idx .. idx + repo_str.len]).value() + else + String.from(""); + + // Find committish in dependency string + const committish = if (committish_str.len > 0) blk: { + const committish_idx = strings.indexOf(dependency, committish_str); + break :blk if (committish_idx) |idx| + sliced.sub(dependency[idx .. idx + committish_str.len]).value() + else + String.from(""); + } else String.from(""); return .{ .literal = sliced.value(), .value = .{ .github = .{ - .owner = sliced.sub(input[0..slash_index]).value(), - .repo = sliced.sub(repo).value(), - .committish = if (hash_index == 0) String.from("") else sliced.sub(input[hash_index + 1 ..]).value(), + .owner = owner, + .repo = repo, + .committish = committish, }, }, .tag = .github, @@ -1454,9 +1434,17 @@ pub const Behavior = packed struct(u8) { } }; +fn hgiToTag(info: hosted_git_info.HostedGitInfo) Version.Tag { + return switch (info.host_provider) { + .github => if (info.default_representation == .shortcut) .github else .git, + .bitbucket, .gitlab, .gist, .sourcehut => .git, + }; +} + const string = []const u8; const Environment = @import("../env.zig"); +const hosted_git_info = @import("./hosted_git_info.zig"); const std = @import("std"); const Repository = @import("./repository.zig").Repository; diff --git a/src/install/hosted_git_info.zig b/src/install/hosted_git_info.zig new file mode 100644 index 0000000000..93cb88cef4 --- /dev/null +++ b/src/install/hosted_git_info.zig @@ -0,0 +1,1750 @@ +//! Resolves Git URLs and metadata. +//! +//! This library mimics https://www.npmjs.com/package/hosted-git-info. At the time of writing, the +//! latest version is 9.0.0. Although @markovejnovic believes there are bugs in the original +//! library, this library aims to be bug-for-bug compatible with the original. +//! +//! One thing that's really notable is that hosted-git-info supports extensions and we currently +//! offer no support for extensions. This could be added in the future if necessary. +//! +//! # Core Concepts +//! +//! The goal of this library is to transform a Git URL or a "shortcut" (which is a shorthand for a +//! longer URL) into a structured representation of the relevant Git repository. +//! +//! ## Shortcuts +//! +//! A shortcut is a shorthand for a longer URL. For example, `github:user/repo` is a shortcut which +//! resolves to a full Github URL. `gitlab:user/repo` is another example of a shortcut. +//! +//! # Types +//! +//! This library revolves around a couple core types which are briefly described here. +//! +//! ## `HostedGitInfo` +//! +//! This is the main API point of this library. It encapsulates information about a Git repository. +//! To parse URLs into this structure, use the `fromUrl` member function. +//! +//! ## `HostProvider` +//! +//! This enumeration defines all the known Git host providers. Each provider has slightly different +//! properties which need to be accounted for. Further details are provided in its documentation. +//! +//! ## `UrlProtocol` +//! +//! This is a type that encapsulates the different types of protocols that a URL may have. This +//! includes three different cases: +//! +//! - `well_defined`: A protocol which is directly supported by this library. +//! - `custom`: A protocol which is not known by this library, but is specified in the URL. +//! TODO(markovejnovic): How is this handled? +//! - `unknown`: A protocol which is not specified in the URL. +//! +//! ## `WellDefinedProtocol` +//! +//! This type represents the set of known protocols by this library. Each protocol has slightly +//! different properties which need to be accounted for. +//! +//! It's noteworthy that `WellDefinedProtocol` doesn't refer to "true" protocols, but includes fake +//! tags like `github:` which are handled as "shortcuts" by this library. + +/// Represents how a URL should be reported when formatting it as a string. +/// +/// Input strings may be given in any format and they may be formatted in any format. If you wish +/// to format a URL in a specific format, you can use its `format*` methods. However, each input +/// string has a "default" representation which is used when calling `toString()`. Depending on the +/// input, the default representation may be different. +const Representation = enum { + /// foo/bar + shortcut, + /// git+ssh://git@domain/user/project.git#committish + sshurl, + /// ssh://domain/user/project.git#committish + ssh, + /// https://domain/user/project.git#committish + https, + /// git://domain/user/project.git#committish + git, + /// http://domain/user/project.git#committish + http, +}; + +pub const HostedGitInfo = struct { + const Self = @This(); + + committish: ?[]const u8, + project: []const u8, + user: ?[]const u8, + host_provider: HostProvider, + default_representation: Representation, + + _memory_buffer: []const u8, + _allocator: std.mem.Allocator, + + /// Helper function to decode a percent-encoded string and append it to a StringBuilder. + /// Returns the decoded slice and updates the StringBuilder's length. + /// + /// The reason we need to do this is because we get URLs like github:user%20name/repo and we + /// need to decode them to 'user name/repo'. It would be nice if we could get all the + /// functionality of jsc.URL WITHOUT the percent-encoding, but alas, we cannot. And we need the + /// jsc.URL functionality for parsing, validating and punycode-decoding the URL. + /// + /// Therefore, we use this function to first take a URL string, encode it into a *jsc.URL and + /// then decode it back to a normal string. Kind of a lot of work, but it works. + fn decodeAndAppend( + sb: *bun.StringBuilder, + input: []const u8, + ) error{ OutOfMemory, InvalidURL }![]const u8 { + const writable = sb.writable(); + var stream = std.io.fixedBufferStream(writable); + const decoded_len = PercentEncoding.decode( + @TypeOf(stream.writer()), + stream.writer(), + input, + ) catch { + return error.InvalidURL; + }; + sb.len += decoded_len; + return writable[0..decoded_len]; + } + + fn copyFrom( + committish: ?[]const u8, + project: []const u8, + user: ?[]const u8, + host_provider: HostProvider, + default_representation: Representation, + allocator: std.mem.Allocator, + ) error{ OutOfMemory, InvalidURL }!Self { + var sb = bun.StringBuilder{}; + + if (user) |u| sb.count(u); + sb.count(project); + if (committish) |c| sb.count(c); + + sb.allocate(allocator) catch return error.OutOfMemory; + + // Decode user, project, committish while copying + const user_part = if (user) |u| try decodeAndAppend(&sb, u) else null; + const project_part = try decodeAndAppend(&sb, project); + const committish_part = if (committish) |c| try decodeAndAppend(&sb, c) else null; + + const owned_buffer = sb.allocatedSlice(); + + return .{ + .committish = committish_part, + .project = project_part, + .user = user_part, + .host_provider = host_provider, + .default_representation = default_representation, + ._memory_buffer = owned_buffer, + ._allocator = allocator, + }; + } + + /// Initialize a HostedGitInfo from an extracted structure. + /// Takes ownership of the extracted structure. + fn moveFromExtracted( + extracted: *HostProvider.Config.formatters.extract.Result, + host_provider: HostProvider, + default_representation: Representation, + ) Self { + const moved = extracted.move(); + return .{ + .committish = extracted.committish, + .project = extracted.project, + .user = extracted.user, + .host_provider = host_provider, + .default_representation = default_representation, + ._memory_buffer = moved.buffer, + ._allocator = moved.allocator, + }; + } + + /// Clean up owned memory + pub fn deinit(self: *const Self) void { + self._allocator.free(self._memory_buffer); + } + + /// Convert this HostedGitInfo to a JavaScript object + pub fn toJS(self: *const Self, go: *jsc.JSGlobalObject) jsc.JSValue { + const obj = jsc.JSValue.createEmptyObject(go, 6); + obj.put( + go, + jsc.ZigString.static("type"), + bun.String.fromBytes(self.host_provider.typeStr()).toJS(go), + ); + obj.put( + go, + jsc.ZigString.static("domain"), + bun.String.fromBytes(self.host_provider.domain()).toJS(go), + ); + obj.put( + go, + jsc.ZigString.static("project"), + bun.String.fromBytes(self.project).toJS(go), + ); + obj.put( + go, + jsc.ZigString.static("user"), + if (self.user) |user| bun.String.fromBytes(user).toJS(go) else .null, + ); + obj.put( + go, + jsc.ZigString.static("committish"), + if (self.committish) |committish| + bun.String.fromBytes(committish).toJS(go) + else + .null, + ); + obj.put( + go, + jsc.ZigString.static("default"), + bun.String.fromBytes(@tagName(self.default_representation)).toJS(go), + ); + + return obj; + } + + pub const StringPair = struct { + save_spec: []const u8, + fetch_spec: ?[]const u8, + }; + + /// Given a URL-like (including shortcuts) string, parses it into a HostedGitInfo structure. + /// The HostedGitInfo is valid only for as long as `git_url` is valid. + pub fn fromUrl( + allocator: std.mem.Allocator, + git_url: []const u8, + ) error{ OutOfMemory, InvalidURL }!?Self { + // git_url_mut may carry two ownership semantics: + // - It aliases `git_url`, in which case it must not be freed. + // - It actually points to a new allocation, in which case it must be freed. + var git_url_mut = git_url; + defer if (git_url.ptr != git_url_mut.ptr) allocator.free(git_url_mut); + + if (isGitHubShorthand(git_url)) { + // In this case we have to prefix the url with `github:`. + // + // NOTE(markovejnovic): I don't exactly understand why this is treated specially. + // + // TODO(markovejnovic): Perhaps we can avoid this allocation... + // This one seems quite easy to get rid of. + git_url_mut = bun.handleOom(bun.strings.concat(allocator, &.{ "github:", git_url })); + } + + const parsed = parseUrl(allocator, git_url_mut) catch { + return null; + }; + defer parsed.url.deinit(); + + const host_provider = switch (parsed.proto) { + .well_formed => |p| p.hostProvider() orelse HostProvider.fromUrlDomain(parsed.url), + .unknown => HostProvider.fromUrlDomain(parsed.url), + .custom => HostProvider.fromUrl(parsed.url), + } orelse return null; + + const is_shortcut = parsed.proto == .well_formed and parsed.proto.well_formed.isShortcut(); + if (!is_shortcut) { + var extracted = try host_provider.extract(allocator, parsed.url) orelse return null; + return HostedGitInfo.moveFromExtracted( + &extracted, + host_provider, + parsed.proto.defaultRepresentation(), + ); + } + + // Shortcut path: github:user/repo, gitlab:user/repo, etc. (from-url.js line 68-96) + const pathname_owned = try parsed.url.pathname().toOwnedSlice(allocator); + defer allocator.free(pathname_owned); + + // Strip leading / (from-url.js line 69) + var pathname = bun.strings.trimPrefixComptime(u8, pathname_owned, "/"); + + // Strip auth (from-url.js line 70-74) + if (bun.strings.indexOfChar(pathname, '@')) |first_at| { + pathname = pathname[first_at + 1 ..]; + } + + // extract user and project from pathname (from-url.js line 76-86) + var user_part: ?[]const u8 = null; + const project_part: []const u8 = blk: { + if (bun.strings.lastIndexOfChar(pathname, '/')) |last_slash| { + const user_str = pathname[0..last_slash]; + // We want nulls only, never empty strings (from-url.js line 79-82) + if (user_str.len > 0) { + user_part = user_str; + } + break :blk pathname[last_slash + 1 ..]; + } else { + break :blk pathname; + } + }; + + // Strip .git suffix (from-url.js line 88-90) + const project_trimmed = bun.strings.trimSuffixComptime(project_part, ".git"); + + // Get committish from URL fragment (from-url.js line 92-94) + const fragment = try parsed.url.fragmentIdentifier().toOwnedSlice(allocator); + defer allocator.free(fragment); + const committish: ?[]const u8 = if (fragment.len > 0) fragment else null; + + // copyFrom will URL-decode user, project, and committish + return try HostedGitInfo.copyFrom( + committish, + project_trimmed, + user_part, + host_provider, + .shortcut, // Shortcuts always use shortcut representation + allocator, + ); + } +}; + +/// Handles input like git:github.com:user/repo and inserting the // after the first : if necessary +/// +/// May error with `error.InvalidGitUrl` if the URL is not valid. +/// +/// Note that this may or may not allocate but it manages its own memory. +fn parseUrl(allocator: std.mem.Allocator, npa_str: []const u8) error{ InvalidGitUrl, OutOfMemory }!struct { + url: *jsc.URL, + proto: UrlProtocol, +} { + // Certain users can provide values like user:password@github.com:foo/bar and we want to + // "correct" the protocol to be git+ssh://user:password@github.com:foo/bar + var proto_pair = normalizeProtocol(npa_str); + defer proto_pair.deinit(); + + // TODO(markovejnovic): We might be able to avoid this allocation if we rework how jsc.URL + // accepts strings. + const maybe_url = proto_pair.toUrl(allocator); + if (maybe_url) |url| return .{ .url = url, .proto = proto_pair.protocol }; + + // Now that may fail, if the URL is not nicely formatted. In that case, we try to correct the + // URL and parse it. + var corrected = try correctUrl(&proto_pair, allocator); + defer corrected.deinit(); + const corrected_url = corrected.toUrl(allocator); + if (corrected_url) |url| return .{ .url = url, .proto = corrected.protocol }; + + // Otherwise, we complain. + return error.InvalidGitUrl; +} + +/// Enumeration of possible URL protocols. +pub const WellDefinedProtocol = enum { + const Self = @This(); + + git, + git_plus_file, + git_plus_ftp, + git_plus_http, + git_plus_https, + git_plus_rsync, + git_plus_ssh, + http, + https, + ssh, + + // Non-standard protocols. + github, + bitbucket, + gitlab, + gist, + sourcehut, + + /// Mapping from protocol string (without colon) to WellDefinedProtocol. + pub const strings = bun.ComptimeStringMap(Self, .{ + .{ "bitbucket", .bitbucket }, + .{ "gist", .gist }, + .{ "git+file", .git_plus_file }, + .{ "git+ftp", .git_plus_ftp }, + .{ "git+http", .git_plus_http }, + .{ "git+https", .git_plus_https }, + .{ "git+rsync", .git_plus_rsync }, + .{ "git+ssh", .git_plus_ssh }, + .{ "git", .git }, + .{ "github", .github }, + .{ "gitlab", .gitlab }, + .{ "http", .http }, + .{ "https", .https }, + .{ "sourcehut", .sourcehut }, + .{ "ssh", .ssh }, + }); + + /// Look up a protocol from a string that includes the trailing colon (e.g., "https:"). + /// This method strips the colon before looking up in the strings map. + pub fn fromStringWithColon(protocol_with_colon: []const u8) ?Self { + return if (protocol_with_colon.len == 0) + return null + else + strings.get(bun.strings.trimSuffixComptime(protocol_with_colon, ":")); + } + + /// Maximum length of any protocol string in the strings map (computed at compile time). + pub const max_protocol_length: comptime_int = blk: { + var max: usize = 0; + for (strings.kvs) |kv| { + if (kv.key.len > max) { + max = kv.key.len; + } + } + break :blk max; + }; + + /// Buffer type for holding a protocol string with colon (e.g., "git+rsync:"). + /// Sized to hold the longest protocol name plus one character for the colon. + pub const StringWithColonBuffer = [max_protocol_length + 1]u8; + + /// Get the protocol string with colon (e.g., "https:") for a given protocol enum. + /// Takes a buffer pointer to hold the result. + /// Returns a slice into that buffer containing the protocol string with colon. + pub fn toStringWithColon(self: Self, buf: *StringWithColonBuffer) []const u8 { + // Look up the protocol string (without colon) from the map + const protocol_str = strings.getKey(self).?; + + // Copy to buffer and append colon + @memcpy(buf[0..protocol_str.len], protocol_str); + buf[protocol_str.len] = ':'; + return buf[0 .. protocol_str.len + 1]; + } + + /// The set of characters that must appear between . + /// For example, in `git+ssh://user@host:repo`, the `//` is the magic string. Some protocols + /// don't support this, for example `github:user/repo` is valid. + /// + /// Kind of arbitrary and implemented to match hosted-git-info's behavior. + fn protocolResourceIdentifierConcatenationToken(self: Self) []const u8 { + return switch (self) { + .git, + .git_plus_file, + .git_plus_ftp, + .git_plus_http, + .git_plus_https, + .git_plus_rsync, + .git_plus_ssh, + .http, + .https, + .ssh, + => "//", + .github, .bitbucket, .gitlab, .gist, .sourcehut => "", + }; + } + + /// Determine the default representation for this protocol. + /// Mirrors the logic in from-url.js line 110. + fn defaultRepresentation(self: Self) Representation { + return switch (self) { + .git_plus_ssh, .ssh, .git_plus_http => .sshurl, + .git_plus_https => .https, + .git_plus_file, .git_plus_ftp, .git_plus_rsync, .git => .git, + .http => .http, + .https => .https, + .github, .bitbucket, .gitlab, .gist, .sourcehut => .shortcut, + }; + } + + /// Certain protocols will have associated host providers. This method returns the associated + /// host provider, if one exists. + fn hostProvider(self: Self) ?HostProvider { + return switch (self) { + .github => .github, + .bitbucket => .bitbucket, + .gitlab => .gitlab, + .gist => .gist, + .sourcehut => .sourcehut, + else => null, + }; + } + + fn isShortcut(self: Self) bool { + return switch (self) { + .github, .bitbucket, .gitlab, .gist, .sourcehut => true, + else => false, + }; + } +}; + +/// Test whether the given node-package-arg string is a GitHub shorthand. +/// +/// This mirrors the implementation of hosted-git-info, though it is significantly faster. +pub fn isGitHubShorthand(npa_str: []const u8) bool { + // The implementation in hosted-git-info is a multi-pass algorithm. We've opted to implement a + // single-pass algorithm for better performance. + // + // This could be even faster with SIMD but this is probably good enough for now. + if (npa_str.len < 1) { + return false; + } + + // Implements doesNotStartWithDot + if (npa_str[0] == '.' or npa_str[0] == '/') { + return false; + } + + var pound_idx: ?usize = null; + var seen_slash = false; + + for (npa_str, 0..) |c, i| { + switch (c) { + // Implement atOnlyAfterHash and colonOnlyAfterHash + ':', '@' => { + if (pound_idx == null) { + return false; + } + }, + + '#' => { + pound_idx = i; + }, + '/' => { + // Implements secondSlashOnlyAfterHash + if (seen_slash and pound_idx == null) { + return false; + } + + seen_slash = true; + }, + else => { + // Implement spaceOnlyAfterHash + if (std.ascii.isWhitespace(c) and pound_idx == null) { + return false; + } + }, + } + } + + // Implements doesNotEndWithSlash + const does_not_end_with_slash = + if (pound_idx) |pi| + npa_str[pi - 1] != '/' + else + npa_str.len >= 1 and npa_str[npa_str.len - 1] != '/'; + + // Implement hasSlash + return seen_slash and does_not_end_with_slash; +} + +const UrlProtocol = union(enum) { + well_formed: WellDefinedProtocol, + + // A protocol which is not known by the library. Includes the : character, but not the + // double-slash, so `foo://bar` would yield `foo:`. + custom: []const u8, + + // Either no protocol was specified or the library couldn't figure it out. + unknown, + + /// Deduces the default representation for this protocol. + pub fn defaultRepresentation(self: UrlProtocol) Representation { + return switch (self) { + .well_formed => self.well_formed.defaultRepresentation(), + else => .sshurl, // Unknown/custom protocols default to sshurl + }; + } +}; + +const UrlProtocolPair = struct { + const Self = @This(); + + url: union(enum) { + managed: struct { + buf: []const u8, + allocator: std.mem.Allocator, + }, + unmanaged: []const u8, + }, + protocol: UrlProtocol, + + pub fn urlSlice(self: *const Self) []const u8 { + return switch (self.url) { + .managed => |s| s.buf, + .unmanaged => |s| s, + }; + } + + pub fn deinit(self: *Self) void { + switch (self.url) { + .managed => |*u| { + u.allocator.free(u.buf); + }, + .unmanaged => |_| {}, + } + } + + /// Given a protocol pair, create a jsc.URL if possible. May allocate, but owns its memory. + fn toUrl(self: *const UrlProtocolPair, allocator: std.mem.Allocator) ?*jsc.URL { + // Ehhh.. Old IE's max path length was 2K so let's just use that. I searched for a + // statistical distribution of URL lengths and found nothing. + const long_url_thresh = 2048; + + var alloc = std.heap.stackFallback(long_url_thresh, allocator); + + var protocol_buf: WellDefinedProtocol.StringWithColonBuffer = undefined; + + return concatPartsToUrl( + alloc.get(), + switch (self.protocol) { + // If we have no protocol, we can assume it is git+ssh. + .unknown => &.{ "git+ssh://", self.urlSlice() }, + .custom => |proto_str| &.{ proto_str, "//", self.urlSlice() }, + // This feels counter-intuitive but is correct. It's not github://foo/bar, it's + // github:foo/bar. + .well_formed => |proto_tag| &.{ + proto_tag.toStringWithColon(&protocol_buf), + // Wordy name for a double-slash or empty string. github:foo/bar is valid, but + // git+ssh://foo/bar is also valid. + proto_tag.protocolResourceIdentifierConcatenationToken(), + self.urlSlice(), + }, + }, + ); + } + + fn concatPartsToUrl(allocator: std.mem.Allocator, parts: []const []const u8) ?*jsc.URL { + // TODO(markovejnovic): There is a sad unnecessary allocation here that I don't know how to + // get rid of -- in theory, URL.zig could allocate once. + const new_str = bun.handleOom(bun.strings.concat(allocator, parts)); + defer allocator.free(new_str); + return jsc.URL.fromString(bun.String.init(new_str)); + } +}; + +/// Given a loose string that may or may not be a valid URL, attempt to normalize it. +/// +/// Returns a struct containing the URL string with the `protocol://` part removed and a tagged +/// enumeration. If the protocol is known, it is returned as a WellDefinedProtocol. If the protocol +/// is specified in the URL, it is given as a slice and if it is not specified, the `unknown` field +/// is returned. The result is a view into `npa_str` which must, consequently, remain stable. +/// +/// This mirrors the `correctProtocol` function in `hosted-git-info/parse-url.js`. +fn normalizeProtocol(npa_str: []const u8) UrlProtocolPair { + var first_colon_idx: i32 = -1; + if (bun.strings.indexOfChar(npa_str, ':')) |idx| { + first_colon_idx = @intCast(idx); + } + + // The cast here is safe -- first_colon_idx is guaranteed to be [-1, infty) + const proto_slice = npa_str[0..@intCast(first_colon_idx + 1)]; + + if (WellDefinedProtocol.fromStringWithColon(proto_slice)) |url_protocol| { + // We need to slice off the protocol from the string. Note there are two very annoying + // cases -- one where the protocol string is foo://bar and one where it is foo:bar. + var post_colon = bun.strings.substring(npa_str, @intCast(first_colon_idx + 1), null); + + return .{ + .url = .{ + .unmanaged = if (bun.strings.hasPrefixComptime(post_colon, "//")) + post_colon[2..post_colon.len] + else + post_colon, + }, + .protocol = .{ .well_formed = url_protocol }, + }; + } + + // Now we search for the @ character to see if we have a user@host:path GIT+SSH style URL. + const first_at_idx = bun.strings.indexOfChar(npa_str, '@'); + if (first_at_idx) |at_idx| { + // We have an @ in the string + if (first_colon_idx != -1) { + // We have a : in the string. + if (at_idx > first_colon_idx) { + // The @ is after the :, so we have something like user:pass@host which is a valid + // URL. and should be promoted to git_plus_ssh. It's guaranteed that the issue is + // not that we have proto://user@host:path because we would've caught that above. + return .{ + .url = .{ .unmanaged = npa_str }, + .protocol = .{ .well_formed = .git_plus_ssh }, + }; + } else { + // Otherwise we have something like user@host:path which is also a valid URL. + // Things are, however, different, since we don't really know what the protocol is. + // Remember, we would've hit the proto://user@host:path above. + + // NOTE(markovejnovic): I don't, at this moment, understand how exactly + // hosted-git-info and npm-package-arg handle this "unknown" protocol as of now. + // We can't really guess either -- there's no :// which comes before @ + return .{ .url = .{ .unmanaged = npa_str }, .protocol = .unknown }; + } + } else { + // Something like user@host which is also a valid URL. Since no :, that means that the + // URL is as good as it gets. No need to slice. + return .{ + .url = .{ .unmanaged = npa_str }, + .protocol = .{ .well_formed = .git_plus_ssh }, + }; + } + } + + // The next thing we can try is to search for the double slash and treat this protocol as a + // custom one. + // + // NOTE(markovejnovic): I also think this is wrong in parse-url.js. + // They: + // 1. Test the protocol against known protocols (which is fine) + // 2. Then, if not found, they go through that hoop of checking for @ and : guessing if it is a + // git+ssh URL or not + // 3. And finally, they search for ://. + // + // The last two steps feel like they should happen in reverse order: + // + // If I have a foobar://user:host@path URL (and foobar is not given as a known protocol), their + // implementation will not report this as a foobar protocol, but rather as + // git+ssh://foobar://user:host@path which, I think, is wrong. + // + // I even tested it: https://tinyurl.com/5y4e6zrw + // + // Our goal is to be bug-for-bug compatible, at least for now, so this is how I re-implemented + // it. + const maybe_dup_slash_idx = bun.strings.indexOf(npa_str, "//"); + if (maybe_dup_slash_idx) |dup_slash_idx| { + if (dup_slash_idx == first_colon_idx + 1) { + return .{ + .url = .{ .unmanaged = bun.strings.substring(npa_str, dup_slash_idx + 2, null) }, + .protocol = .{ .custom = npa_str[0..dup_slash_idx] }, + }; + } + } + + // Well, otherwise we have to split the original URL into two pieces, + // right at the colon. + if (first_colon_idx != -1) { + return .{ + .url = .{ + .unmanaged = bun.strings.substring(npa_str, @intCast(first_colon_idx + 1), null), + }, + .protocol = .{ .custom = npa_str[0..@intCast(first_colon_idx + 1)] }, + }; + } + + // Well we couldn't figure out anything. + return .{ .url = .{ .unmanaged = npa_str }, .protocol = .unknown }; +} + +/// Attempt to correct an scp-style URL into a proper URL, parsable with jsc.URL. Potentially +/// mutates the original input. +/// +/// This function assumes that the input is an scp-style URL. +fn correctUrl( + url_proto_pair: *const UrlProtocolPair, + allocator: std.mem.Allocator, +) error{OutOfMemory}!UrlProtocolPair { + const at_idx: isize = if (bun.strings.lastIndexBeforeChar( + url_proto_pair.urlSlice(), + '@', + '#', + )) |idx| + @intCast(idx) + else + -1; + + const col_idx: isize = if (bun.strings.lastIndexBeforeChar( + url_proto_pair.urlSlice(), + ':', + '#', + )) |idx| + @intCast(idx) + else + -1; + + if (col_idx > at_idx) { + var duped = try allocator.dupe(u8, url_proto_pair.urlSlice()); + duped[@intCast(col_idx)] = '/'; + + return .{ + .url = .{ + .managed = .{ + .buf = duped, + .allocator = allocator, + }, + }, + .protocol = .{ .well_formed = .git_plus_ssh }, + }; + } + + if (col_idx == -1 and url_proto_pair.protocol == .unknown) { + return .{ + .url = url_proto_pair.url, + .protocol = .{ .well_formed = .git_plus_ssh }, + }; + } + + return .{ .url = url_proto_pair.url, .protocol = url_proto_pair.protocol }; +} + +/// This enumeration encapsulates all known host providers and their configurations. +/// +/// Providers each have different configuration fields and, on top of that, have different +/// mechanisms for formatting URLs. For example, GitHub will format SSH URLs as +/// `git+ssh://git@${domain}/${user}/${project}.git${maybeJoin('#', committish)}`, while `gist` +/// will format URLs as `git+ssh://git@${domain}/${project}.git${maybeJoin('#', committish)}`. This +/// structure encapsulates the differences between providers and how they handle all of that. +/// +/// Effectively, this enumeration acts as a registry of all known providers and a vtable for +/// jumping between different behavior for different providers. +const HostProvider = enum { + const Self = @This(); + + bitbucket, + gist, + github, + gitlab, + sourcehut, + + fn formatSsh( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + return configs.get(self).format_ssh(self, allocator, user, project, committish); + } + + fn formatSshUrl( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + return configs.get(self).format_sshurl(self, allocator, user, project, committish); + } + + fn formatHttps( + self: Self, + allocator: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + return configs.get(self).format_https(self, allocator, auth, user, project, committish); + } + + fn formatShortcut( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + return configs.get(self).format_shortcut(self, allocator, user, project, committish); + } + + fn extract( + self: Self, + allocator: std.mem.Allocator, + url: *jsc.URL, + ) error{ OutOfMemory, InvalidURL }!?Config.formatters.extract.Result { + return configs.get(self).format_extract(allocator, url); + } + + const Config = struct { + protocols: []const WellDefinedProtocol, + domain: []const u8, + shortcut: []const u8, + tree_path: ?[]const u8, + blob_path: ?[]const u8, + edit_path: ?[]const u8, + + format_ssh: formatters.ssh.Type = Self.Config.formatters.ssh.default, + format_sshurl: formatters.ssh_url.Type = Self.Config.formatters.ssh_url.default, + format_https: formatters.https.Type = Self.Config.formatters.https.default, + format_shortcut: formatters.shortcut.Type = Self.Config.formatters.shortcut.default, + format_git: formatters.git.Type = Self.Config.formatters.git.default, + format_extract: formatters.extract.Type, + + /// Encapsulates all the various foramtters that different hosts may have. Usually this has + /// to do with URLs, but could be other things. + const formatters = struct { + fn requiresUser(user: ?[]const u8) void { + if (user == null) { + @panic("Attempted to format a default SSH URL without a user. This is an " ++ + "irrecoverable programming bug in Bun. Please report this issue " ++ + "on GitHub."); + } + } + + /// Mirrors hosts.js's sshtemplate + const ssh = struct { + const Type = *const fn ( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8; + + fn default( + self: Self, + alloc: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + requiresUser(user); + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + alloc, + "git@{s}:{s}/{s}.git{s}{s}", + .{ self.domain(), user.?, project, cmsh_sep, cmsh }, + ); + } + + fn gist( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + _ = user; + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + allocator, + "git@{s}:{s}.git{s}{s}", + .{ self.domain(), project, cmsh_sep, cmsh }, + ); + } + }; + + /// Mirrors hosts.js's sshurltemplate + const ssh_url = struct { + const Type = *const fn ( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8; + + fn default( + self: Self, + alloc: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + requiresUser(user); + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + alloc, + "git+ssh://git@{s}/{s}/{s}.git{s}{s}", + .{ self.domain(), user.?, project, cmsh_sep, cmsh }, + ); + } + + fn gist( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + _ = user; + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + allocator, + "git+ssh://git@{s}/{s}.git{s}{s}", + .{ self.domain(), project, cmsh_sep, cmsh }, + ); + } + }; + + /// Mirrors hosts.js's httpstemplate + const https = struct { + const Type = *const fn ( + self: Self, + allocator: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8; + + fn default( + self: Self, + alloc: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + requiresUser(user); + + const auth_str = if (auth) |a| a else ""; + const auth_sep = if (auth_str.len > 0) "@" else ""; + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + alloc, + "git+https://{s}{s}{s}/{s}/{s}.git{s}{s}", + .{ auth_str, auth_sep, self.domain(), user.?, project, cmsh_sep, cmsh }, + ); + } + + fn gist( + self: Self, + alloc: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + _ = auth; + _ = user; + + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + alloc, + "git+https://{s}/{s}.git{s}{s}", + .{ self.domain(), project, cmsh_sep, cmsh }, + ); + } + + fn sourcehut( + self: Self, + alloc: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + requiresUser(user); + _ = auth; + + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + alloc, + "https://{s}/{s}/{s}.git{s}{s}", + .{ self.domain(), user.?, project, cmsh_sep, cmsh }, + ); + } + }; + + /// Mirrors hosts.js's shortcuttemplate + const shortcut = struct { + const Type = *const fn ( + self: Self, + allocator: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8; + + fn default( + self: Self, + alloc: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + requiresUser(user); + + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + alloc, + "{s}{s}/{s}{s}{s}", + .{ self.shortcut(), user.?, project, cmsh_sep, cmsh }, + ); + } + + fn gist( + self: Self, + alloc: std.mem.Allocator, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + _ = user; + + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + alloc, + "{s}{s}{s}{s}", + .{ self.shortcut(), project, cmsh_sep, cmsh }, + ); + } + }; + + /// Mirrors hosts.js's extract function + const extract = struct { + const Result = struct { + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + _owned_buffer: ?[]const u8, + _allocator: std.mem.Allocator, + + fn deinit(self: *Result) void { + if (self._owned_buffer) |buf| { + self._allocator.free(buf); + } + } + + /// Return the buffer which owns this Result and the allocator responsible for + /// freeing it. + /// + /// Same semantics as C++ STL. Safe-to-deinit Result after this, not safe to + /// use it. + fn move(self: *Result) struct { + buffer: []const u8, + allocator: std.mem.Allocator, + } { + if (self._owned_buffer == null) { + @panic("Cannot move an empty Result. This is a bug in Bun. Please " ++ + "report this issue on GitHub."); + } + + const buffer = self._owned_buffer.?; + const allocator = self._allocator; + + self._owned_buffer = null; + + return .{ + .buffer = buffer, + .allocator = allocator, + }; + } + }; + + const Type = *const fn ( + allocator: std.mem.Allocator, + url: *jsc.URL, + ) error{ OutOfMemory, InvalidURL }!?Result; + + fn github( + allocator: std.mem.Allocator, + url: *jsc.URL, + ) error{ OutOfMemory, InvalidURL }!?Result { + const pathname_owned = try url.pathname().toOwnedSlice(allocator); + defer allocator.free(pathname_owned); + const pathname = bun.strings.trimPrefixComptime(u8, pathname_owned, "/"); + + var iter = std.mem.splitScalar(u8, pathname, '/'); + const user_part = iter.next() orelse return null; + const project_part = iter.next() orelse return null; + const type_part = iter.next(); + const committish_part = iter.next(); + + const project = bun.strings.trimSuffixComptime(project_part, ".git"); + + if (user_part.len == 0 or project.len == 0) { + return null; + } + + // If the type part says something other than "tree", we're not looking at a + // github URL that we understand. + if (type_part) |tp| { + if (!std.mem.eql(u8, tp, "tree")) { + return null; + } + } + + var committish: ?[]const u8 = null; + if (type_part == null) { + const fragment_str = url.fragmentIdentifier(); + defer fragment_str.deref(); + const fragment_utf8 = fragment_str.toUTF8(allocator); + defer fragment_utf8.deinit(); + const fragment = fragment_utf8.slice(); + if (fragment.len > 0) { + committish = fragment; + } + } else { + committish = committish_part; + } + + var sb = bun.StringBuilder{}; + sb.count(user_part); + sb.count(project); + if (committish) |c| sb.count(c); + + try sb.allocate(allocator); + + const user_slice = try HostedGitInfo.decodeAndAppend(&sb, user_part); + const project_slice = try HostedGitInfo.decodeAndAppend(&sb, project); + const committish_slice = + if (committish) |c| + try HostedGitInfo.decodeAndAppend(&sb, c) + else + null; + + return .{ + .user = user_slice, + .project = project_slice, + .committish = committish_slice, + ._owned_buffer = sb.allocatedSlice(), + ._allocator = allocator, + }; + } + + fn bitbucket( + allocator: std.mem.Allocator, + url: *jsc.URL, + ) error{ InvalidURL, OutOfMemory }!?Result { + const pathname_owned = try url.pathname().toOwnedSlice(allocator); + defer allocator.free(pathname_owned); + const pathname = bun.strings.trimPrefixComptime(u8, pathname_owned, "/"); + + var iter = std.mem.splitScalar(u8, pathname, '/'); + const user_part = iter.next() orelse return null; + const project_part = iter.next() orelse return null; + const aux = iter.next(); + + if (aux) |a| { + if (std.mem.eql(u8, a, "get")) { + return null; + } + } + + const project = bun.strings.trimSuffixComptime(project_part, ".git"); + + if (user_part.len == 0 or project.len == 0) { + return null; + } + + const fragment_str = url.fragmentIdentifier(); + defer fragment_str.deref(); + const fragment_utf8 = fragment_str.toUTF8(allocator); + defer fragment_utf8.deinit(); + const fragment = fragment_utf8.slice(); + const committish = if (fragment.len > 0) fragment else null; + + var sb = bun.StringBuilder{}; + sb.count(user_part); + sb.count(project); + if (committish) |c| sb.count(c); + + try sb.allocate(allocator); + + const user_slice = try HostedGitInfo.decodeAndAppend(&sb, user_part); + const project_slice = try HostedGitInfo.decodeAndAppend(&sb, project); + const committish_slice = + if (committish) |c| + try HostedGitInfo.decodeAndAppend(&sb, c) + else + null; + + return .{ + .user = user_slice, + .project = project_slice, + .committish = committish_slice, + ._owned_buffer = sb.allocatedSlice(), + ._allocator = allocator, + }; + } + + fn gitlab( + allocator: std.mem.Allocator, + url: *jsc.URL, + ) error{ OutOfMemory, InvalidURL }!?Result { + const pathname_owned = try url.pathname().toOwnedSlice(allocator); + defer allocator.free(pathname_owned); + const pathname = bun.strings.trimPrefixComptime(u8, pathname_owned, "/"); + + if (bun.strings.contains(pathname, "/-/") or + bun.strings.contains(pathname, "/archive.tar.gz")) + { + return null; + } + + const end_slash = bun.strings.lastIndexOfChar(pathname, '/') orelse return null; + const project_part = pathname[end_slash + 1 ..]; + const user_part = pathname[0..end_slash]; + + const project = bun.strings.trimSuffixComptime(project_part, ".git"); + + if (user_part.len == 0 or project.len == 0) { + return null; + } + + const fragment_str = url.fragmentIdentifier(); + defer fragment_str.deref(); + const fragment_utf8 = fragment_str.toUTF8(allocator); + defer fragment_utf8.deinit(); + const committish = fragment_utf8.slice(); + + var sb = bun.StringBuilder{}; + sb.count(user_part); + sb.count(project); + if (committish.len > 0) sb.count(committish); + + try sb.allocate(allocator); + + const user_slice = try HostedGitInfo.decodeAndAppend(&sb, user_part); + const project_slice = try HostedGitInfo.decodeAndAppend(&sb, project); + const committish_slice = + if (committish.len > 0) + HostedGitInfo.decodeAndAppend(&sb, committish) catch return null + else + null; + + return .{ + .user = user_slice, + .project = project_slice, + .committish = committish_slice, + ._owned_buffer = sb.allocatedSlice(), + ._allocator = allocator, + }; + } + + fn gist( + allocator: std.mem.Allocator, + url: *jsc.URL, + ) error{ OutOfMemory, InvalidURL }!?Result { + const pathname_owned = try url.pathname().toOwnedSlice(allocator); + defer allocator.free(pathname_owned); + const pathname = bun.strings.trimPrefixComptime(u8, pathname_owned, "/"); + + var iter = std.mem.splitScalar(u8, pathname, '/'); + var user_part = iter.next() orelse return null; + var project_part = iter.next(); + const aux = iter.next(); + + if (aux) |a| { + if (std.mem.eql(u8, a, "raw")) { + return null; + } + } + + if (project_part == null or project_part.?.len == 0) { + project_part = user_part; + user_part = ""; + } + + const project = bun.strings.trimSuffixComptime(project_part.?, ".git"); + const user = if (user_part.len > 0) user_part else null; + + if (project.len == 0) { + return null; + } + + const fragment_str = url.fragmentIdentifier(); + defer fragment_str.deref(); + const fragment_utf8 = fragment_str.toUTF8(allocator); + defer fragment_utf8.deinit(); + const fragment = fragment_utf8.slice(); + const committish = if (fragment.len > 0) fragment else null; + + var sb = bun.StringBuilder{}; + if (user) |u| sb.count(u); + sb.count(project); + if (committish) |c| sb.count(c); + + sb.allocate(allocator) catch return null; + + const user_slice = + if (user) |u| + HostedGitInfo.decodeAndAppend(&sb, u) catch return null + else + null; + const project_slice = + HostedGitInfo.decodeAndAppend(&sb, project) catch return null; + const committish_slice = + if (committish) |c| + HostedGitInfo.decodeAndAppend(&sb, c) catch return null + else + null; + + return .{ + .user = user_slice, + .project = project_slice, + .committish = committish_slice, + ._owned_buffer = sb.allocatedSlice(), + ._allocator = allocator, + }; + } + + fn sourcehut( + allocator: std.mem.Allocator, + url: *jsc.URL, + ) error{ InvalidURL, OutOfMemory }!?Result { + const pathname_owned = try url.pathname().toOwnedSlice(allocator); + defer allocator.free(pathname_owned); + const pathname = bun.strings.trimPrefixComptime(u8, pathname_owned, "/"); + + var iter = std.mem.splitScalar(u8, pathname, '/'); + const user_part = iter.next() orelse return null; + const project_part = iter.next() orelse return null; + const aux = iter.next(); + + if (aux) |a| { + if (std.mem.eql(u8, a, "archive")) { + return null; + } + } + + const project = bun.strings.trimSuffixComptime(project_part, ".git"); + + if (user_part.len == 0 or project.len == 0) { + return null; + } + + const fragment_str = url.fragmentIdentifier(); + defer fragment_str.deref(); + const fragment_utf8 = fragment_str.toUTF8(allocator); + defer fragment_utf8.deinit(); + const fragment = fragment_utf8.slice(); + const committish = if (fragment.len > 0) fragment else null; + + var sb = bun.StringBuilder{}; + sb.count(user_part); + sb.count(project); + if (committish) |c| sb.count(c); + + sb.allocate(allocator) catch return null; + + const user_slice = blk: { + const writable = sb.writable(); + var stream = std.io.fixedBufferStream(writable); + const decoded_len = PercentEncoding.decode( + @TypeOf(stream.writer()), + stream.writer(), + user_part, + ) catch return null; + sb.len += decoded_len; + break :blk writable[0..decoded_len]; + }; + const project_slice = blk: { + const writable = sb.writable(); + var stream = std.io.fixedBufferStream(writable); + const decoded_len = PercentEncoding.decode( + @TypeOf(stream.writer()), + stream.writer(), + project, + ) catch return null; + sb.len += decoded_len; + break :blk writable[0..decoded_len]; + }; + const committish_slice = if (committish) |c| blk: { + const writable = sb.writable(); + var stream = std.io.fixedBufferStream(writable); + const decoded_len = PercentEncoding.decode( + @TypeOf(stream.writer()), + stream.writer(), + c, + ) catch return null; + sb.len += decoded_len; + break :blk writable[0..decoded_len]; + } else null; + + return .{ + .user = user_slice, + .project = project_slice, + .committish = committish_slice, + ._owned_buffer = sb.allocatedSlice(), + ._allocator = allocator, + }; + } + }; + + /// Mirrors hosts.js's gittemplate + const git = struct { + const Type = ?*const fn ( + self: Self, + allocator: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8; + + const default: Type = null; + + fn github( + self: Self, + allocator: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + requiresUser(user); + + const auth_str = if (auth) |a| a else ""; + const auth_sep = if (auth_str.len > 0) "@" else ""; + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + allocator, + "git://{s}{s}{s}/{s}/{s}.git{s}{s}", + .{ auth_str, auth_sep, self.domain(), user.?, project, cmsh_sep, cmsh }, + ); + } + + fn gist( + self: Self, + allocator: std.mem.Allocator, + auth: ?[]const u8, + user: ?[]const u8, + project: []const u8, + committish: ?[]const u8, + ) error{OutOfMemory}![]const u8 { + _ = auth; + _ = user; + + const cmsh: []const u8 = if (committish) |c| c else ""; + const cmsh_sep = if (cmsh.len > 0) "#" else ""; + + return std.fmt.allocPrint( + allocator, + "git://{s}/{s}.git{s}{s}", + .{ self.domain(), project, cmsh_sep, cmsh }, + ); + } + }; + }; + }; + + const configs = std.enums.EnumArray(Self, Config).init(.{ + .bitbucket = .{ + .protocols = &.{ .git_plus_http, .git_plus_https, .ssh, .https }, + .domain = "bitbucket.org", + .shortcut = "bitbucket:", + .tree_path = "src", + .blob_path = "src", + .edit_path = "?mode=edit", + .format_extract = Self.Config.formatters.extract.bitbucket, + }, + .gist = .{ + .protocols = &.{ .git, .git_plus_ssh, .git_plus_https, .ssh, .https }, + .domain = "gist.github.com", + .shortcut = "gist:", + .tree_path = null, + .blob_path = null, + .edit_path = "edit", + .format_ssh = Self.Config.formatters.ssh.gist, + .format_sshurl = Self.Config.formatters.ssh_url.gist, + .format_https = Self.Config.formatters.https.gist, + .format_shortcut = Self.Config.formatters.shortcut.gist, + .format_git = Self.Config.formatters.git.gist, + .format_extract = Self.Config.formatters.extract.gist, + }, + .github = .{ + .protocols = &.{ .git, .http, .git_plus_ssh, .git_plus_https, .ssh, .https }, + .domain = "github.com", + .shortcut = "github:", + .tree_path = "tree", + .blob_path = "blob", + .edit_path = "edit", + .format_git = Self.Config.formatters.git.github, + .format_extract = Self.Config.formatters.extract.github, + }, + .gitlab = .{ + .protocols = &.{ .git_plus_ssh, .git_plus_https, .ssh, .https }, + .domain = "gitlab.com", + .shortcut = "gitlab:", + .tree_path = "tree", + .blob_path = "tree", + .edit_path = "-/edit", + .format_extract = Self.Config.formatters.extract.gitlab, + }, + .sourcehut = .{ + .protocols = &.{ .git_plus_ssh, .https }, + .domain = "git.sr.ht", + .shortcut = "sourcehut:", + .tree_path = "tree", + .blob_path = "tree", + .edit_path = null, + .format_https = Self.Config.formatters.https.sourcehut, + .format_extract = Self.Config.formatters.extract.sourcehut, + }, + }); + + /// Return the string representation of the provider. + fn typeStr(self: Self) []const u8 { + return @tagName(self); + } + + fn shortcut(self: Self) []const u8 { + return configs.get(self).shortcut; + } + + fn domain(self: Self) []const u8 { + return configs.get(self).domain; + } + + fn protocols(self: Self) []const WellDefinedProtocol { + return configs.get(self).protocols; + } + + fn shortcutWithoutColon(self: Self) []const u8 { + const shct = self.shortcut(); + return shct[0 .. shct.len - 1]; + } + + fn treePath(self: Self) ?[]const u8 { + return configs.get(self).tree_path; + } + + fn blobPath(self: Self) ?[]const u8 { + return configs.get(self).blob_path; + } + + fn editPath(self: Self) ?[]const u8 { + return configs.get(self).edit_path; + } + + /// Find the appropriate host provider by its shortcut (e.g. "github:"). + /// + /// The second parameter allows you to declare whether the given string includes the protocol: + /// colon or not. + fn fromShortcut( + shortcut_str: []const u8, + comptime with_colon: enum { with_colon, without_colon }, + ) ?HostProvider { + inline for (std.meta.fields(Self)) |field| { + const provider: HostProvider = @enumFromInt(field.value); + + const shortcut_matches = std.mem.eql( + u8, + switch (with_colon) { + .with_colon => provider.shortcut(), + .without_colon => provider.shortcutWithoutColon(), + }, + shortcut_str, + ); + + if (shortcut_matches) { + return provider; + } + } + + return null; + } + + /// Find the appropriate host provider by its domain (e.g. "github.com"). + fn fromDomain(domain_str: []const u8) ?HostProvider { + inline for (std.meta.fields(Self)) |field| { + const provider: HostProvider = @enumFromInt(field.value); + + if (std.mem.eql(u8, provider.domain(), domain_str)) { + return provider; + } + } + + return null; + } + + /// Parse a URL and return the appropriate host provider, if any. + fn fromUrl(url: *jsc.URL) ?HostProvider { + const proto_str = url.protocol(); + defer proto_str.deref(); + + // Try shortcut first (github:, gitlab:, etc.) + if (HostProvider.fromShortcut(proto_str.byteSlice(), .without_colon)) |provider| { + return provider; + } + + return HostProvider.fromUrlDomain(url); + } + + // Given a URL, use the domain in the URL to find the appropriate host provider. + fn fromUrlDomain(url: *jsc.URL) ?HostProvider { + const max_hostname_len: comptime_int = 253; + + const hostname_str = url.hostname(); + defer hostname_str.deref(); + + var fba_mem: [max_hostname_len]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&fba_mem); + const hostname_utf8 = hostname_str.toUTF8(fba.allocator()); + defer hostname_utf8.deinit(); + const hostname = bun.strings.withoutPrefixComptime(hostname_utf8.slice(), "www."); + + return HostProvider.fromDomain(hostname); + } +}; + +pub const TestingAPIs = struct { + pub fn jsParseUrl(go: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + const allocator = bun.default_allocator; + + if (callframe.argumentsCount() != 1) { + return go.throw("hostedGitInfo.prototype.parseUrl takes exactly 1 argument", .{}); + } + + const arg0 = callframe.argument(0); + if (!arg0.isString()) { + return go.throw( + "hostedGitInfo.prototype.parseUrl takes a string as its " ++ + "first argument", + .{}, + ); + } + + // TODO(markovejnovic): This feels like there's too much going on all + // to give us a slice. Maybe there's a better way to code this up. + const npa_str = try arg0.toBunString(go); + defer npa_str.deref(); + var as_utf8 = npa_str.toUTF8(allocator); + defer as_utf8.deinit(); + const parsed = parseUrl(allocator, as_utf8.mut()) catch |err| { + return go.throw("Invalid Git URL: {}", .{err}); + }; + defer parsed.url.deinit(); + + return parsed.url.href().toJS(go); + } + + pub fn jsFromUrl(go: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue { + const allocator = bun.default_allocator; + + // TODO(markovejnovic): The original hosted-git-info actually takes another argument that + // allows you to inject options. Seems untested so we didn't implement + // it. + if (callframe.argumentsCount() != 1) { + return go.throw("hostedGitInfo.prototype.fromUrl takes exactly 1 argument", .{}); + } + + const arg0 = callframe.argument(0); + if (!arg0.isString()) { + return go.throw( + "hostedGitInfo.prototype.fromUrl takes a string as its first argument", + .{}, + ); + } + + // TODO(markovejnovic): This feels like there's too much going on all to give us a slice. + // Maybe there's a better way to code this up. + const npa_str = try arg0.toBunString(go); + defer npa_str.deref(); + var as_utf8 = npa_str.toUTF8(allocator); + defer as_utf8.deinit(); + const parsed = HostedGitInfo.fromUrl(allocator, as_utf8.mut()) catch |err| { + return go.throw("Invalid Git URL: {}", .{err}); + } orelse { + return .null; + }; + + return parsed.toJS(go); + } +}; + +const std = @import("std"); +const PercentEncoding = @import("../url.zig").PercentEncoding; + +const bun = @import("bun"); +const jsc = bun.jsc; diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 1f3d147594..89b22f5841 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -210,3 +210,8 @@ export const structuredCloneAdvanced: ( ) => any = $newCppFunction("StructuredClone.cpp", "jsFunctionStructuredCloneAdvanced", 5); export const lsanDoLeakCheck = $newCppFunction("InternalForTesting.cpp", "jsFunction_lsanDoLeakCheck", 1); + +export const hostedGitInfo = { + parseUrl: $newZigFunction("hosted_git_info.zig", "TestingAPIs.jsParseUrl", 1), + fromUrl: $newZigFunction("hosted_git_info.zig", "TestingAPIs.jsFromUrl", 1), +}; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 941b73daa3..9c7a023fe2 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -37,6 +37,16 @@ inline fn nqlAtIndexCaseInsensitive(comptime string_count: comptime_int, index: return false; } +/// The given string contains separators that match the platform's path separator style. +pub fn hasPlatformPathSeparators(input_path: []const u8) bool { + if (bun.Environment.isWindows) { + // Windows accepts both forward and backward slashes as path separators + return bun.strings.indexOfAny(input_path, "\\/") != null; + } else { + return bun.strings.containsChar(input_path, '/'); + } +} + const IsSeparatorFunc = fn (char: u8) bool; const IsSeparatorFuncT = fn (comptime T: type, char: anytype) bool; const LastSeparatorFunction = fn (slice: []const u8) ?usize; diff --git a/src/string/immutable.zig b/src/string/immutable.zig index ce36729315..211f5484a0 100644 --- a/src/string/immutable.zig +++ b/src/string/immutable.zig @@ -414,6 +414,12 @@ pub fn indexOfSigned(self: string, str: string) i32 { return @as(i32, @intCast(i)); } +/// Returns last index of `char` before a character `before`. +pub fn lastIndexBeforeChar(in: []const u8, char: u8, before: u8) ?usize { + const before_pos = indexOfChar(in, before) orelse in.len; + return lastIndexOfChar(in[0..before_pos], char); +} + pub fn lastIndexOfChar(self: []const u8, char: u8) callconv(bun.callconv_inline) ?usize { if (comptime Environment.isLinux) { if (@inComptime()) { @@ -1132,6 +1138,15 @@ pub fn index(self: string, str: string) i32 { } } +/// Returns a substring starting at `start` up to the end of the string. +/// If `start` is greater than the string's length, returns an empty string. +pub fn substring(self: anytype, start: ?usize, stop: ?usize) @TypeOf(self) { + const sta = start orelse 0; + const sto = stop orelse self.len; + + return self[@min(sta, self.len)..@min(sto, self.len)]; +} + pub const ascii_vector_size = if (Environment.isWasm) 8 else 16; pub const ascii_u16_vector_size = if (Environment.isWasm) 4 else 8; pub const AsciiVectorInt = std.meta.Int(.unsigned, ascii_vector_size); @@ -1553,6 +1568,13 @@ pub fn trimPrefixComptime(comptime T: type, buffer: []const T, comptime prefix: buffer; } +pub fn trimSuffixComptime(buffer: []const u8, comptime suffix: anytype) []const u8 { + return if (hasSuffixComptime(buffer, suffix)) + buffer[0 .. buffer.len - suffix.len] + else + buffer; +} + /// Get the line number and the byte offsets of `line_range_count` above the desired line number /// The final element is the end index of the desired line const LineRange = struct { @@ -1759,6 +1781,10 @@ pub fn trim(slice: anytype, comptime values_to_strip: []const u8) @TypeOf(slice) return slice[begin..end]; } +pub fn trimSpaces(slice: anytype) @TypeOf(slice) { + return trim(slice, &whitespace_chars); +} + pub fn isAllWhitespace(slice: []const u8) bool { var begin: usize = 0; while (begin < slice.len and std.mem.indexOfScalar(u8, &whitespace_chars, slice[begin]) != null) : (begin += 1) {} @@ -2020,7 +2046,7 @@ pub fn concatWithLength( allocator: std.mem.Allocator, args: []const string, length: usize, -) ![]u8 { +) bun.OOM![]u8 { const out = try allocator.alloc(u8, length); var remain = out; for (args) |arg| { @@ -2034,7 +2060,7 @@ pub fn concatWithLength( pub fn concat( allocator: std.mem.Allocator, args: []const string, -) ![]u8 { +) bun.OOM![]u8 { var length: usize = 0; for (args) |arg| { length += arg.len; @@ -2342,7 +2368,6 @@ pub const toNTPath16 = paths_.toNTPath16; pub const toPath = paths_.toPath; pub const toPathMaybeDir = paths_.toPathMaybeDir; pub const toPathNormalized = paths_.toPathNormalized; -pub const toWDirNormalized = paths_.toWDirNormalized; pub const toWDirPath = paths_.toWDirPath; pub const toWPath = paths_.toWPath; pub const toWPathMaybeDir = paths_.toWPathMaybeDir; diff --git a/src/string/immutable/paths.zig b/src/string/immutable/paths.zig index 8cd11483b7..a541460f39 100644 --- a/src/string/immutable/paths.zig +++ b/src/string/immutable/paths.zig @@ -233,26 +233,6 @@ pub fn normalizeSlashesOnly(buf: []u8, utf8: []const u8, comptime desired_slash: return normalizeSlashesOnlyT(u8, buf, utf8, desired_slash, false); } -pub fn toWDirNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { - var renormalized: ?*bun.PathBuffer = null; - defer if (renormalized) |r| bun.path_buffer_pool.put(r); - - var path_to_use = utf8; - - if (bun.strings.containsChar(utf8, '/')) { - renormalized = bun.path_buffer_pool.get(); - @memcpy(renormalized.?[0..utf8.len], utf8); - for (renormalized.?[0..utf8.len]) |*c| { - if (c.* == '/') { - c.* = '\\'; - } - } - path_to_use = renormalized.?[0..utf8.len]; - } - - return toWDirPath(wbuf, path_to_use); -} - pub fn toWPath(wbuf: []u16, utf8: []const u8) [:0]u16 { return toWPathMaybeDir(wbuf, utf8, false); } diff --git a/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap b/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap deleted file mode 100644 index 96abf0eb3e..0000000000 --- a/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap +++ /dev/null @@ -1,397 +0,0 @@ -// Bun Snapshot v1, https://bun.sh/docs/test/snapshots - -exports[`npa @scoped/package 1`] = ` -{ - "name": "@scoped/package", - "version": { - "name": "@scoped/package", - "tag": "latest", - "type": "dist_tag", - }, -} -`; - -exports[`npa @scoped/package@1.0.0 1`] = ` -{ - "name": "@scoped/package", - "version": { - "alias": false, - "name": "@scoped/package", - "type": "npm", - "version": "==1.0.0", - }, -} -`; - -exports[`npa @scoped/package@1.0.0-beta.1 1`] = ` -{ - "name": "@scoped/package", - "version": { - "alias": false, - "name": "@scoped/package", - "type": "npm", - "version": "==1.0.0-beta.1", - }, -} -`; - -exports[`npa @scoped/package@1.0.0-beta.1+build.123 1`] = ` -{ - "name": "@scoped/package", - "version": { - "alias": false, - "name": "@scoped/package", - "type": "npm", - "version": "==1.0.0-beta.1+build.123", - }, -} -`; - -exports[`npa package 1`] = ` -{ - "name": "package", - "version": { - "name": "package", - "tag": "latest", - "type": "dist_tag", - }, -} -`; - -exports[`npa package@1.0.0 1`] = ` -{ - "name": "package", - "version": { - "alias": false, - "name": "package", - "type": "npm", - "version": "==1.0.0", - }, -} -`; - -exports[`npa package@1.0.0-beta.1 1`] = ` -{ - "name": "package", - "version": { - "alias": false, - "name": "package", - "type": "npm", - "version": "==1.0.0-beta.1", - }, -} -`; - -exports[`npa package@1.0.0-beta.1+build.123 1`] = ` -{ - "name": "package", - "version": { - "alias": false, - "name": "package", - "type": "npm", - "version": "==1.0.0-beta.1+build.123", - }, -} -`; - -exports[`npa bitbucket:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "", - "repo": "bitbucket:dylan-conway/public-install-test", - "type": "git", - }, -} -`; - -exports[`npa bitbucket.org:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "", - "repo": "bitbucket.org:dylan-conway/public-install-test", - "type": "git", - }, -} -`; - -exports[`npa bitbucket.com:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "", - "repo": "bitbucket.com:dylan-conway/public-install-test", - "type": "git", - }, -} -`; - -exports[`npa git@bitbucket.org:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "", - "repo": "git@bitbucket.org:dylan-conway/public-install-test", - "type": "git", - }, -} -`; - -exports[`npa foo/bar 1`] = ` -{ - "name": "", - "version": { - "owner": "foo", - "ref": "", - "repo": "bar", - "type": "github", - }, -} -`; - -exports[`npa gitlab:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "", - "repo": "gitlab:dylan-conway/public-install-test", - "type": "git", - }, -} -`; - -exports[`npa gitlab.com:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "", - "repo": "gitlab.com:dylan-conway/public-install-test", - "type": "git", - }, -} -`; - -exports[`npa http://localhost:5000/no-deps/-/no-deps-2.0.0.tgz 1`] = ` -{ - "name": "", - "version": { - "name": "", - "type": "tarball", - "url": "http://localhost:5000/no-deps/-/no-deps-2.0.0.tgz", - }, -} -`; - -exports[`npa https://registry.npmjs.org/no-deps/-/no-deps-2.0.0.tgz 1`] = ` -{ - "name": "", - "version": { - "name": "", - "type": "tarball", - "url": "https://registry.npmjs.org/no-deps/-/no-deps-2.0.0.tgz", - }, -} -`; - -exports[`npa file:./path/to/tarball.tgz 1`] = ` -{ - "name": "", - "version": { - "name": "", - "path": "./path/to/tarball.tgz", - "type": "tarball", - }, -} -`; - -exports[`npa ./path/to/tarball.tgz 1`] = ` -{ - "name": "", - "version": { - "name": "", - "path": "./path/to/tarball.tgz", - "type": "tarball", - }, -} -`; - -exports[`npa foo/bar 2`] = ` -{ - "name": "", - "version": { - "owner": "foo", - "ref": "", - "repo": "bar", - "type": "github", - }, -} -`; - -exports[`npa github:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "dylan-conway", - "ref": "", - "repo": "public-install-test", - "type": "github", - }, -} -`; - -exports[`npa git@github.com:dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "", - "repo": "git@github.com:dylan-conway/public-install-test", - "type": "git", - }, -} -`; - -exports[`npa https://github.com/dylan-conway/public-install-test 1`] = ` -{ - "name": "", - "version": { - "owner": "dylan-conway", - "ref": "", - "repo": "public-install-test", - "type": "github", - }, -} -`; - -exports[`npa https://github.com/dylan-conway/public-install-test.git 1`] = ` -{ - "name": "", - "version": { - "owner": "dylan-conway", - "ref": "", - "repo": "public-install-test", - "type": "github", - }, -} -`; - -exports[`npa https://github.com/dylan-conway/public-install-test.git#semver:^1.0.0 1`] = ` -{ - "name": "", - "version": { - "owner": "", - "ref": "semver:^1.0.0", - "repo": "https://github.com/dylan-conway/public-install-test.git", - "type": "git", - }, -} -`; - -exports[`dependencies: {"foo": "1.2.3"} 1`] = ` -{ - "alias": false, - "name": "foo", - "type": "npm", - "version": "==1.2.3-foo", -} -`; - -exports[`dependencies: {"foo": "latest"} 1`] = ` -{ - "name": "foo", - "tag": "latest", - "type": "dist_tag", -} -`; - -exports[`dependencies: {"foo": "workspace:*"} 1`] = ` -{ - "name": "*foo", - "type": "workspace", -} -`; - -exports[`dependencies: {"foo": "workspace:^1.0.0"} 1`] = ` -{ - "name": "^1.0.0foo", - "type": "workspace", -} -`; - -exports[`dependencies: {"foo": "workspace:1.0.0"} 1`] = ` -{ - "name": "1.0.0foo", - "type": "workspace", -} -`; - -exports[`dependencies: {"foo": "workspace:1.0.0-beta.1"} 1`] = ` -{ - "name": "1.0.0-beta.1foo", - "type": "workspace", -} -`; - -exports[`dependencies: {"foo": "workspace:1.0.0-beta.1+build.123"} 1`] = ` -{ - "name": "1.0.0-beta.1+build.123foo", - "type": "workspace", -} -`; - -exports[`dependencies: {"foo": "workspace:1.0.0-beta.1+build.123"} 2`] = ` -{ - "name": "1.0.0-beta.1+build.123foo", - "type": "workspace", -} -`; - -exports[`dependencies: {"foo": "workspace:1.0.0-beta.1+build.123"} 3`] = ` -{ - "name": "1.0.0-beta.1+build.123foo", - "type": "workspace", -} -`; - -exports[`dependencies: {"bar": "^1.0.0"} 1`] = ` -{ - "alias": false, - "name": "bar", - "type": "npm", - "version": ">=1.0.0-bar <2.0.0", -} -`; - -exports[`dependencies: {"bar": "~1.0.0"} 1`] = ` -{ - "alias": false, - "name": "bar", - "type": "npm", - "version": ">=1.0.0-bar <1.1.0", -} -`; - -exports[`dependencies: {"bar": "> 1.0.0 < 2.0.0"} 1`] = ` -{ - "alias": false, - "name": "bar", - "type": "npm", - "version": ">1.0.0 && <2.0.0-bar", -} -`; - -exports[`dependencies: {"bar": "1.0.0 - 2.0.0"} 1`] = ` -{ - "alias": false, - "name": "bar", - "type": "npm", - "version": ">=1.0.0 <=2.0.0-bar", -} -`; diff --git a/test/cli/install/bun-install-dep.test.ts b/test/cli/install/bun-install-dep.test.ts deleted file mode 100644 index 03321aa867..0000000000 --- a/test/cli/install/bun-install-dep.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { npa } from "bun:internal-for-testing"; -import { expect, test } from "bun:test"; - -const bitbucket = [ - "bitbucket:dylan-conway/public-install-test", - "bitbucket.org:dylan-conway/public-install-test", - "bitbucket.com:dylan-conway/public-install-test", - "git@bitbucket.org:dylan-conway/public-install-test", -]; - -const tarball_remote = [ - "http://localhost:5000/no-deps/-/no-deps-2.0.0.tgz", - "https://registry.npmjs.org/no-deps/-/no-deps-2.0.0.tgz", -]; - -const local_tarball = ["file:./path/to/tarball.tgz", "./path/to/tarball.tgz"]; -const github = ["foo/bar"]; -const folder = ["file:./path/to/folder"]; - -const gitlab = ["gitlab:dylan-conway/public-install-test", "gitlab.com:dylan-conway/public-install-test"]; - -const all = [ - "@scoped/package", - "@scoped/package@1.0.0", - "@scoped/package@1.0.0-beta.1", - "@scoped/package@1.0.0-beta.1+build.123", - "package", - "package@1.0.0", - "package@1.0.0-beta.1", - "package@1.0.0-beta.1+build.123", - ...bitbucket, - ...github, - ...gitlab, - ...tarball_remote, - ...local_tarball, - ...github, - "github:dylan-conway/public-install-test", - "git@github.com:dylan-conway/public-install-test", - "https://github.com/dylan-conway/public-install-test", - "https://github.com/dylan-conway/public-install-test.git", - "https://github.com/dylan-conway/public-install-test.git#semver:^1.0.0", -]; - -test.each(all)("npa %s", dep => { - expect(npa(dep)).toMatchSnapshot(); -}); - -const pkgJsonLike = [ - ["foo", "1.2.3"], - ["foo", "latest"], - ["foo", "workspace:*"], - ["foo", "workspace:^1.0.0"], - ["foo", "workspace:1.0.0"], - ["foo", "workspace:1.0.0-beta.1"], - ["foo", "workspace:1.0.0-beta.1+build.123"], - ["foo", "workspace:1.0.0-beta.1+build.123"], - ["foo", "workspace:1.0.0-beta.1+build.123"], - ["bar", "^1.0.0"], - ["bar", "~1.0.0"], - ["bar", "> 1.0.0 < 2.0.0"], - ["bar", "1.0.0 - 2.0.0"], -]; - -test.each(pkgJsonLike)('dependencies: {"%s": "%s"}', (name, version) => { - expect(npa(name, version)).toMatchSnapshot(); -}); - -test("bad", () => { - expect(() => npa("-123!}{P}{!P#$s")).toThrow(); -}); diff --git a/test/cli/install/hosted-git-info/cases.ts b/test/cli/install/hosted-git-info/cases.ts new file mode 100644 index 0000000000..7f8a296d97 --- /dev/null +++ b/test/cli/install/hosted-git-info/cases.ts @@ -0,0 +1,2472 @@ +/** + * Contains all the possible test cases that hosted-git-archive.test.ts tests against. + * + * These are organized according to the structure in https://github.com/npm/hosted-git-info/blob/main/test/ at the time + * of writing. + * + * TODO(markovejnovic): This does not include the following set of tests: + * - https://github.com/npm/hosted-git-info/blob/main/test/file.js + * - https://github.com/npm/hosted-git-info/blob/main/test/parse-url.js + */ +// This is a valid git branch name that contains other occurences of the characters we check +// for to determine the committish in order to test that we parse those correctly +const committishDefaults = { committish: "lk/br@nch.t#st:^1.0.0-pre.4" }; + +type Provider = "bitbucket" | "gist" | "github" | "gitlab" | "sourcehut" | "misc"; + +const defaults = { + bitbucket: { type: "bitbucket", user: "foo", project: "bar" }, + gist: { type: "gist", user: null, project: "feedbeef" }, + github: { type: "github", user: "foo", project: "bar" }, + gitlab: { type: "gitlab", user: "foo", project: "bar" }, + gitlabSubgroup: { type: "gitlab", user: "foo/bar", project: "baz" }, + sourcehut: { type: "sourcehut", user: "~foo", project: "bar" }, +}; + +export const validGitUrls: { [K in Provider]: { [K in string]: object } } = { + bitbucket: { + // shortcuts + // + // NOTE auth is accepted but ignored + "bitbucket:foo/bar": { ...defaults.bitbucket, default: "shortcut" }, + "bitbucket:foo/bar#branch": { ...defaults.bitbucket, default: "shortcut", committish: "branch" }, + "bitbucket:user@foo/bar": { ...defaults.bitbucket, default: "shortcut", auth: null }, + "bitbucket:user@foo/bar#branch": { ...defaults.bitbucket, default: "shortcut", auth: null, committish: "branch" }, + "bitbucket:user:password@foo/bar": { ...defaults.bitbucket, default: "shortcut", auth: null }, + "bitbucket:user:password@foo/bar#branch": { + ...defaults.bitbucket, + default: "shortcut", + auth: null, + committish: "branch", + }, + "bitbucket::password@foo/bar": { ...defaults.bitbucket, default: "shortcut", auth: null }, + "bitbucket::password@foo/bar#branch": { + ...defaults.bitbucket, + default: "shortcut", + auth: null, + committish: "branch", + }, + + "bitbucket:foo/bar.git": { ...defaults.bitbucket, default: "shortcut" }, + "bitbucket:foo/bar.git#branch": { ...defaults.bitbucket, default: "shortcut", committish: "branch" }, + "bitbucket:user@foo/bar.git": { ...defaults.bitbucket, default: "shortcut", auth: null }, + "bitbucket:user@foo/bar.git#branch": { + ...defaults.bitbucket, + default: "shortcut", + auth: null, + committish: "branch", + }, + "bitbucket:user:password@foo/bar.git": { ...defaults.bitbucket, default: "shortcut", auth: null }, + "bitbucket:user:password@foo/bar.git#branch": { + ...defaults.bitbucket, + default: "shortcut", + auth: null, + committish: "branch", + }, + "bitbucket::password@foo/bar.git": { ...defaults.bitbucket, default: "shortcut", auth: null }, + "bitbucket::password@foo/bar.git#branch": { + ...defaults.bitbucket, + default: "shortcut", + auth: null, + committish: "branch", + }, + + // no-protocol git+ssh + // + // NOTE auth is accepted but ignored + "git@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "user@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "user@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "user:password@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "user:password@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + ":password@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "user@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "user@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "user:password@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "user:password@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + ":password@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // git+ssh urls + // + // NOTE auth is accepted but ignored + "git+ssh://bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl" }, + "git+ssh://bitbucket.org:foo/bar#branch": { ...defaults.bitbucket, default: "sshurl", committish: "branch" }, + "git+ssh://user@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git+ssh://user@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git+ssh://user:password@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git+ssh://:password@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git+ssh://bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl" }, + "git+ssh://bitbucket.org:foo/bar.git#branch": { ...defaults.bitbucket, default: "sshurl", committish: "branch" }, + "git+ssh://user@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git+ssh://user@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git+ssh://user:password@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "git+ssh://:password@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // ssh urls + // + // NOTE auth is accepted but ignored + "ssh://bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl" }, + "ssh://bitbucket.org:foo/bar#branch": { ...defaults.bitbucket, default: "sshurl", committish: "branch" }, + "ssh://user@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "ssh://user@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "ssh://user:password@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@bitbucket.org:foo/bar": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "ssh://:password@bitbucket.org:foo/bar#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "ssh://bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl" }, + "ssh://bitbucket.org:foo/bar.git#branch": { ...defaults.bitbucket, default: "sshurl", committish: "branch" }, + "ssh://user@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "ssh://user@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "ssh://user:password@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@bitbucket.org:foo/bar.git": { ...defaults.bitbucket, default: "sshurl", auth: null }, + "ssh://:password@bitbucket.org:foo/bar.git#branch": { + ...defaults.bitbucket, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // git+https urls + // + // NOTE auth is accepted and respected + "git+https://bitbucket.org/foo/bar": { ...defaults.bitbucket, default: "https" }, + "git+https://bitbucket.org/foo/bar#branch": { ...defaults.bitbucket, default: "https", committish: "branch" }, + "git+https://user@bitbucket.org/foo/bar": { ...defaults.bitbucket, default: "https", auth: "user" }, + "git+https://user@bitbucket.org/foo/bar#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@bitbucket.org/foo/bar": { + ...defaults.bitbucket, + default: "https", + auth: "user:password", + }, + "git+https://user:password@bitbucket.org/foo/bar#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@bitbucket.org/foo/bar": { ...defaults.bitbucket, default: "https", auth: ":password" }, + "git+https://:password@bitbucket.org/foo/bar#branch": { + ...defaults.bitbucket, + default: "https", + auth: ":password", + committish: "branch", + }, + + "git+https://bitbucket.org/foo/bar.git": { ...defaults.bitbucket, default: "https" }, + "git+https://bitbucket.org/foo/bar.git#branch": { ...defaults.bitbucket, default: "https", committish: "branch" }, + "git+https://user@bitbucket.org/foo/bar.git": { ...defaults.bitbucket, default: "https", auth: "user" }, + "git+https://user@bitbucket.org/foo/bar.git#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@bitbucket.org/foo/bar.git": { + ...defaults.bitbucket, + default: "https", + auth: "user:password", + }, + "git+https://user:password@bitbucket.org/foo/bar.git#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@bitbucket.org/foo/bar.git": { ...defaults.bitbucket, default: "https", auth: ":password" }, + "git+https://:password@bitbucket.org/foo/bar.git#branch": { + ...defaults.bitbucket, + default: "https", + auth: ":password", + committish: "branch", + }, + + // https urls + // + // NOTE auth is accepted and respected + "https://bitbucket.org/foo/bar": { ...defaults.bitbucket, default: "https" }, + "https://bitbucket.org/foo/bar#branch": { ...defaults.bitbucket, default: "https", committish: "branch" }, + "https://user@bitbucket.org/foo/bar": { ...defaults.bitbucket, default: "https", auth: "user" }, + "https://user@bitbucket.org/foo/bar#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@bitbucket.org/foo/bar": { ...defaults.bitbucket, default: "https", auth: "user:password" }, + "https://user:password@bitbucket.org/foo/bar#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@bitbucket.org/foo/bar": { ...defaults.bitbucket, default: "https", auth: ":password" }, + "https://:password@bitbucket.org/foo/bar#branch": { + ...defaults.bitbucket, + default: "https", + auth: ":password", + committish: "branch", + }, + + "https://bitbucket.org/foo/bar.git": { ...defaults.bitbucket, default: "https" }, + "https://bitbucket.org/foo/bar.git#branch": { ...defaults.bitbucket, default: "https", committish: "branch" }, + "https://user@bitbucket.org/foo/bar.git": { ...defaults.bitbucket, default: "https", auth: "user" }, + "https://user@bitbucket.org/foo/bar.git#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@bitbucket.org/foo/bar.git": { + ...defaults.bitbucket, + default: "https", + auth: "user:password", + }, + "https://user:password@bitbucket.org/foo/bar.git#branch": { + ...defaults.bitbucket, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@bitbucket.org/foo/bar.git": { ...defaults.bitbucket, default: "https", auth: ":password" }, + "https://:password@bitbucket.org/foo/bar.git#branch": { + ...defaults.bitbucket, + default: "https", + auth: ":password", + committish: "branch", + }, + }, + gist: { + // shortcuts + // + // NOTE auth is accepted but ignored + "gist:feedbeef": { ...defaults.gist, default: "shortcut" }, + "gist:feedbeef#branch": { ...defaults.gist, default: "shortcut", committish: "branch" }, + "gist:user@feedbeef": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user@feedbeef#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + "gist:user:password@feedbeef": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user:password@feedbeef#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + "gist::password@feedbeef": { ...defaults.gist, default: "shortcut", auth: null }, + "gist::password@feedbeef#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + + "gist:feedbeef.git": { ...defaults.gist, default: "shortcut" }, + "gist:feedbeef.git#branch": { ...defaults.gist, default: "shortcut", committish: "branch" }, + "gist:user@feedbeef.git": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user@feedbeef.git#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + "gist:user:password@feedbeef.git": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user:password@feedbeef.git#branch": { + ...defaults.gist, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gist::password@feedbeef.git": { ...defaults.gist, default: "shortcut", auth: null }, + "gist::password@feedbeef.git#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + + "gist:/feedbeef": { ...defaults.gist, default: "shortcut" }, + "gist:/feedbeef#branch": { ...defaults.gist, default: "shortcut", committish: "branch" }, + "gist:user@/feedbeef": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user@/feedbeef#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + "gist:user:password@/feedbeef": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user:password@/feedbeef#branch": { + ...defaults.gist, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gist::password@/feedbeef": { ...defaults.gist, default: "shortcut", auth: null }, + "gist::password@/feedbeef#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + + "gist:/feedbeef.git": { ...defaults.gist, default: "shortcut" }, + "gist:/feedbeef.git#branch": { ...defaults.gist, default: "shortcut", committish: "branch" }, + "gist:user@/feedbeef.git": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user@/feedbeef.git#branch": { ...defaults.gist, default: "shortcut", auth: null, committish: "branch" }, + "gist:user:password@/feedbeef.git": { ...defaults.gist, default: "shortcut", auth: null }, + "gist:user:password@/feedbeef.git#branch": { + ...defaults.gist, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gist::password@/feedbeef.git": { ...defaults.gist, default: "shortcut", auth: null }, + "gist::password@/feedbeef.git#branch": { + ...defaults.gist, + default: "shortcut", + auth: null, + committish: "branch", + }, + + "gist:foo/feedbeef": { ...defaults.gist, default: "shortcut", user: "foo" }, + "gist:foo/feedbeef#branch": { ...defaults.gist, default: "shortcut", user: "foo", committish: "branch" }, + "gist:user@foo/feedbeef": { ...defaults.gist, default: "shortcut", user: "foo", auth: null }, + "gist:user@foo/feedbeef#branch": { + ...defaults.gist, + default: "shortcut", + user: "foo", + auth: null, + committish: "branch", + }, + "gist:user:password@foo/feedbeef": { ...defaults.gist, default: "shortcut", user: "foo", auth: null }, + "gist:user:password@foo/feedbeef#branch": { + ...defaults.gist, + default: "shortcut", + user: "foo", + auth: null, + committish: "branch", + }, + "gist::password@foo/feedbeef": { ...defaults.gist, default: "shortcut", user: "foo", auth: null }, + "gist::password@foo/feedbeef#branch": { + ...defaults.gist, + default: "shortcut", + user: "foo", + auth: null, + committish: "branch", + }, + + "gist:foo/feedbeef.git": { ...defaults.gist, default: "shortcut", user: "foo" }, + "gist:foo/feedbeef.git#branch": { ...defaults.gist, default: "shortcut", user: "foo", committish: "branch" }, + "gist:user@foo/feedbeef.git": { ...defaults.gist, default: "shortcut", user: "foo", auth: null }, + "gist:user@foo/feedbeef.git#branch": { + ...defaults.gist, + default: "shortcut", + user: "foo", + auth: null, + committish: "branch", + }, + "gist:user:password@foo/feedbeef.git": { ...defaults.gist, default: "shortcut", user: "foo", auth: null }, + "gist:user:password@foo/feedbeef.git#branch": { + ...defaults.gist, + default: "shortcut", + user: "foo", + auth: null, + committish: "branch", + }, + "gist::password@foo/feedbeef.git": { ...defaults.gist, default: "shortcut", user: "foo", auth: null }, + "gist::password@foo/feedbeef.git#branch": { + ...defaults.gist, + default: "shortcut", + user: "foo", + auth: null, + committish: "branch", + }, + + // git urls + // + // NOTE auth is accepted and respected + "git://gist.github.com/feedbeef": { ...defaults.gist, default: "git" }, + "git://gist.github.com/feedbeef#branch": { ...defaults.gist, default: "git", committish: "branch" }, + "git://user@gist.github.com/feedbeef": { ...defaults.gist, default: "git", auth: "user" }, + "git://user@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "git", + auth: "user", + committish: "branch", + }, + "git://user:password@gist.github.com/feedbeef": { ...defaults.gist, default: "git", auth: "user:password" }, + "git://user:password@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "git", + auth: "user:password", + committish: "branch", + }, + "git://:password@gist.github.com/feedbeef": { ...defaults.gist, default: "git", auth: ":password" }, + "git://:password@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "git", + auth: ":password", + committish: "branch", + }, + + "git://gist.github.com/feedbeef.git": { ...defaults.gist, default: "git" }, + "git://gist.github.com/feedbeef.git#branch": { ...defaults.gist, default: "git", committish: "branch" }, + "git://user@gist.github.com/feedbeef.git": { ...defaults.gist, default: "git", auth: "user" }, + "git://user@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "git", + auth: "user", + committish: "branch", + }, + "git://user:password@gist.github.com/feedbeef.git": { ...defaults.gist, default: "git", auth: "user:password" }, + "git://user:password@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "git", + auth: "user:password", + committish: "branch", + }, + "git://:password@gist.github.com/feedbeef.git": { ...defaults.gist, default: "git", auth: ":password" }, + "git://:password@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "git", + auth: ":password", + committish: "branch", + }, + + "git://gist.github.com/foo/feedbeef": { ...defaults.gist, default: "git", user: "foo" }, + "git://gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "git", + user: "foo", + committish: "branch", + }, + "git://user@gist.github.com/foo/feedbeef": { ...defaults.gist, default: "git", user: "foo", auth: "user" }, + "git://user@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "git", + user: "foo", + auth: "user", + committish: "branch", + }, + "git://user:password@gist.github.com/foo/feedbeef": { + ...defaults.gist, + default: "git", + user: "foo", + auth: "user:password", + }, + "git://user:password@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "git", + user: "foo", + auth: "user:password", + committish: "branch", + }, + "git://:password@gist.github.com/foo/feedbeef": { + ...defaults.gist, + default: "git", + user: "foo", + auth: ":password", + }, + "git://:password@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "git", + user: "foo", + auth: ":password", + committish: "branch", + }, + + "git://gist.github.com/foo/feedbeef.git": { ...defaults.gist, default: "git", user: "foo" }, + "git://gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "git", + user: "foo", + committish: "branch", + }, + "git://user@gist.github.com/foo/feedbeef.git": { ...defaults.gist, default: "git", user: "foo", auth: "user" }, + "git://user@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "git", + user: "foo", + auth: "user", + committish: "branch", + }, + "git://user:password@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "git", + user: "foo", + auth: "user:password", + }, + "git://user:password@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "git", + user: "foo", + auth: "user:password", + committish: "branch", + }, + "git://:password@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "git", + user: "foo", + auth: ":password", + }, + "git://:password@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "git", + user: "foo", + auth: ":password", + committish: "branch", + }, + + // no-protocol git+ssh + // + // NOTE auth is accepted and ignored + "git@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "git@gist.github.com:feedbeef#branch": { ...defaults.gist, default: "sshurl", auth: null, committish: "branch" }, + "user@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "user@gist.github.com:feedbeef#branch": { ...defaults.gist, default: "sshurl", auth: null, committish: "branch" }, + "user:password@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "user:password@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + ":password@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "git@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + committish: "branch", + auth: null, + }, + "user@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "user@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "user:password@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "user:password@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + ":password@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git@gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "git@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "user@gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "user@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "user:password@gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "user:password@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + ":password@gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + ":password@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + + "git@gist.github.com:foo/feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "git@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "user@gist.github.com:foo/feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "user@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "user:password@gist.github.com:foo/feedbeef.git": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "user:password@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + ":password@gist.github.com:foo/feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + ":password@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + + // git+ssh urls + // + // NOTE auth is accepted but ignored + // NOTE see TODO at list of invalids, some inputs fail and shouldn't + "git+ssh://gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://user@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://user:password@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://:password@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git+ssh://gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://user@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://user:password@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "git+ssh://:password@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git+ssh://gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", user: "foo" }, + "git+ssh://gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + user: "foo", + committish: "branch", + }, + "git+ssh://user@gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "git+ssh://user@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "git+ssh://user:password@gist.github.com:foo/feedbeef": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "git+ssh://user:password@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "git+ssh://:password@gist.github.com:foo/feedbeef": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "git+ssh://:password@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + + "git+ssh://gist.github.com:foo/feedbeef.git": { ...defaults.gist, default: "sshurl", user: "foo" }, + "git+ssh://gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + user: "foo", + committish: "branch", + }, + "git+ssh://user@gist.github.com:foo/feedbeef.git": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "git+ssh://user@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "git+ssh://user:password@gist.github.com:foo/feedbeef.git": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "git+ssh://user:password@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "git+ssh://:password@gist.github.com:foo/feedbeef.git": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "git+ssh://:password@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + + // ssh urls + // + // NOTE auth is accepted but ignored + "ssh://gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://user@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://user:password@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@gist.github.com:feedbeef": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://:password@gist.github.com:feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "ssh://gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://user@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://user:password@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@gist.github.com:feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null }, + "ssh://:password@gist.github.com:feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "ssh://gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", user: "foo" }, + "ssh://gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + user: "foo", + committish: "branch", + }, + "ssh://user@gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "ssh://user@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "ssh://user:password@gist.github.com:foo/feedbeef": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "ssh://user:password@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "ssh://:password@gist.github.com:foo/feedbeef": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "ssh://:password@gist.github.com:foo/feedbeef#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + + "ssh://gist.github.com:foo/feedbeef.git": { ...defaults.gist, default: "sshurl", user: "foo" }, + "ssh://gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + user: "foo", + committish: "branch", + }, + "ssh://user@gist.github.com:foo/feedbeef.git": { ...defaults.gist, default: "sshurl", auth: null, user: "foo" }, + "ssh://user@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "ssh://user:password@gist.github.com:foo/feedbeef.git": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "ssh://user:password@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + "ssh://:password@gist.github.com:foo/feedbeef.git": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + }, + "ssh://:password@gist.github.com:foo/feedbeef.git#branch": { + ...defaults.gist, + default: "sshurl", + auth: null, + user: "foo", + committish: "branch", + }, + + // git+https urls + // + // NOTE auth is accepted and respected + "git+https://gist.github.com/feedbeef": { ...defaults.gist, default: "https" }, + "git+https://gist.github.com/feedbeef#branch": { ...defaults.gist, default: "https", committish: "branch" }, + "git+https://user@gist.github.com/feedbeef": { ...defaults.gist, default: "https", auth: "user" }, + "git+https://user@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@gist.github.com/feedbeef": { + ...defaults.gist, + default: "https", + auth: "user:password", + }, + "git+https://user:password@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@gist.github.com/feedbeef": { ...defaults.gist, default: "https", auth: ":password" }, + "git+https://:password@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + committish: "branch", + }, + + "git+https://gist.github.com/feedbeef.git": { ...defaults.gist, default: "https" }, + "git+https://gist.github.com/feedbeef.git#branch": { ...defaults.gist, default: "https", committish: "branch" }, + "git+https://user@gist.github.com/feedbeef.git": { ...defaults.gist, default: "https", auth: "user" }, + "git+https://user@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@gist.github.com/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: "user:password", + }, + "git+https://user:password@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@gist.github.com/feedbeef.git": { ...defaults.gist, default: "https", auth: ":password" }, + "git+https://:password@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + committish: "branch", + }, + + "git+https://gist.github.com/foo/feedbeef": { ...defaults.gist, default: "https", user: "foo" }, + "git+https://gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + user: "foo", + committish: "branch", + }, + "git+https://user@gist.github.com/foo/feedbeef": { + ...defaults.gist, + default: "https", + auth: "user", + user: "foo", + }, + "git+https://user@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user", + user: "foo", + committish: "branch", + }, + "git+https://user:password@gist.github.com/foo/feedbeef": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + }, + "git+https://user:password@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + committish: "branch", + }, + "git+https://:password@gist.github.com/foo/feedbeef": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + }, + "git+https://:password@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + committish: "branch", + }, + + "git+https://gist.github.com/foo/feedbeef.git": { ...defaults.gist, default: "https", user: "foo" }, + "git+https://gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + user: "foo", + committish: "branch", + }, + "git+https://user@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: "user", + user: "foo", + }, + "git+https://user@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user", + user: "foo", + committish: "branch", + }, + "git+https://user:password@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + }, + "git+https://user:password@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + committish: "branch", + }, + "git+https://:password@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + }, + "git+https://:password@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + committish: "branch", + }, + + // https urls + // + // NOTE auth is accepted and respected + "https://gist.github.com/feedbeef": { ...defaults.gist, default: "https" }, + "https://gist.github.com/feedbeef#branch": { ...defaults.gist, default: "https", committish: "branch" }, + "https://user@gist.github.com/feedbeef": { ...defaults.gist, default: "https", auth: "user" }, + "https://user@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@gist.github.com/feedbeef": { ...defaults.gist, default: "https", auth: "user:password" }, + "https://user:password@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@gist.github.com/feedbeef": { ...defaults.gist, default: "https", auth: ":password" }, + "https://:password@gist.github.com/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + committish: "branch", + }, + + "https://gist.github.com/feedbeef.git": { ...defaults.gist, default: "https" }, + "https://gist.github.com/feedbeef.git#branch": { ...defaults.gist, default: "https", committish: "branch" }, + "https://user@gist.github.com/feedbeef.git": { ...defaults.gist, default: "https", auth: "user" }, + "https://user@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@gist.github.com/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: "user:password", + }, + "https://user:password@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@gist.github.com/feedbeef.git": { ...defaults.gist, default: "https", auth: ":password" }, + "https://:password@gist.github.com/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + committish: "branch", + }, + + "https://gist.github.com/foo/feedbeef": { ...defaults.gist, default: "https", user: "foo" }, + "https://gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + user: "foo", + committish: "branch", + }, + "https://user@gist.github.com/foo/feedbeef": { ...defaults.gist, default: "https", auth: "user", user: "foo" }, + "https://user@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user", + user: "foo", + committish: "branch", + }, + "https://user:password@gist.github.com/foo/feedbeef": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + }, + "https://user:password@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + committish: "branch", + }, + "https://:password@gist.github.com/foo/feedbeef": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + }, + "https://:password@gist.github.com/foo/feedbeef#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + committish: "branch", + }, + + "https://gist.github.com/foo/feedbeef.git": { ...defaults.gist, default: "https", user: "foo" }, + "https://gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + user: "foo", + committish: "branch", + }, + "https://user@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: "user", + user: "foo", + }, + "https://user@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user", + user: "foo", + committish: "branch", + }, + "https://user:password@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + }, + "https://user:password@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: "user:password", + user: "foo", + committish: "branch", + }, + "https://:password@gist.github.com/foo/feedbeef.git": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + }, + "https://:password@gist.github.com/foo/feedbeef.git#branch": { + ...defaults.gist, + default: "https", + auth: ":password", + user: "foo", + committish: "branch", + }, + }, + github: { + // shortcuts + // + // NOTE auth is accepted but ignored + "github:foo/bar": { ...defaults.github, default: "shortcut" }, + [`github:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + ...committishDefaults, + }, + "github:user@foo/bar": { ...defaults.github, default: "shortcut", auth: null }, + [`github:user@foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + auth: null, + ...committishDefaults, + }, + "github:user:password@foo/bar": { ...defaults.github, default: "shortcut", auth: null }, + [`github:user:password@foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + auth: null, + ...committishDefaults, + }, + "github::password@foo/bar": { ...defaults.github, default: "shortcut", auth: null }, + [`github::password@foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + auth: null, + ...committishDefaults, + }, + + "github:foo/bar.git": { ...defaults.github, default: "shortcut" }, + [`github:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + ...committishDefaults, + }, + "github:user@foo/bar.git": { ...defaults.github, default: "shortcut", auth: null }, + [`github:user@foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + auth: null, + ...committishDefaults, + }, + "github:user:password@foo/bar.git": { ...defaults.github, default: "shortcut", auth: null }, + [`github:user:password@foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + auth: null, + ...committishDefaults, + }, + "github::password@foo/bar.git": { ...defaults.github, default: "shortcut", auth: null }, + [`github::password@foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "shortcut", + auth: null, + ...committishDefaults, + }, + + // git urls + // + // NOTE auth is accepted and respected + "git://github.com/foo/bar": { ...defaults.github, default: "git" }, + [`git://github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + ...committishDefaults, + }, + "git://user@github.com/foo/bar": { ...defaults.github, default: "git", auth: "user" }, + [`git://user@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + auth: "user", + ...committishDefaults, + }, + "git://user:password@github.com/foo/bar": { ...defaults.github, default: "git", auth: "user:password" }, + [`git://user:password@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + auth: "user:password", + ...committishDefaults, + }, + "git://:password@github.com/foo/bar": { ...defaults.github, default: "git", auth: ":password" }, + [`git://:password@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + auth: ":password", + ...committishDefaults, + }, + + "git://github.com/foo/bar.git": { ...defaults.github, default: "git" }, + [`git://github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + ...committishDefaults, + }, + "git://git@github.com/foo/bar.git": { ...defaults.github, default: "git", auth: "git" }, + [`git://git@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + auth: "git", + ...committishDefaults, + }, + "git://user:password@github.com/foo/bar.git": { ...defaults.github, default: "git", auth: "user:password" }, + [`git://user:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + auth: "user:password", + ...committishDefaults, + }, + "git://:password@github.com/foo/bar.git": { ...defaults.github, default: "git", auth: ":password" }, + [`git://:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "git", + auth: ":password", + ...committishDefaults, + }, + + // no-protocol git+ssh + // + // NOTE auth is _required_ (see invalid list) but ignored + "user@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`user@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "user:password@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`user:password@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + ":password@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`:password@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + + "user@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`user@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "user:password@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`user:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + ":password@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + + // git+ssh urls + // + // NOTE auth is accepted but ignored + "git+ssh://github.com:foo/bar": { ...defaults.github, default: "sshurl" }, + [`git+ssh://github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + ...committishDefaults, + }, + "git+ssh://user@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`git+ssh://user@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "git+ssh://user:password@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`git+ssh://user:password@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "git+ssh://:password@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`git+ssh://:password@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + + "git+ssh://github.com:foo/bar.git": { ...defaults.github, default: "sshurl" }, + [`git+ssh://github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + ...committishDefaults, + }, + "git+ssh://user@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`git+ssh://user@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "git+ssh://user:password@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`git+ssh://user:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "git+ssh://:password@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`git+ssh://:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + + // ssh urls + // + // NOTE auth is accepted but ignored + "ssh://github.com:foo/bar": { ...defaults.github, default: "sshurl" }, + [`ssh://github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + ...committishDefaults, + }, + "ssh://user@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`ssh://user@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "ssh://user:password@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`ssh://user:password@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "ssh://:password@github.com:foo/bar": { ...defaults.github, default: "sshurl", auth: null }, + [`ssh://:password@github.com:foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + + "ssh://github.com:foo/bar.git": { ...defaults.github, default: "sshurl" }, + [`ssh://github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + ...committishDefaults, + }, + "ssh://user@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`ssh://user@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "ssh://user:password@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`ssh://user:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + "ssh://:password@github.com:foo/bar.git": { ...defaults.github, default: "sshurl", auth: null }, + [`ssh://:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "sshurl", + auth: null, + ...committishDefaults, + }, + + // git+https urls + // + // NOTE auth is accepted and respected + "git+https://github.com/foo/bar": { ...defaults.github, default: "https" }, + [`git+https://github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + ...committishDefaults, + }, + "git+https://user@github.com/foo/bar": { ...defaults.github, default: "https", auth: "user" }, + [`git+https://user@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user", + ...committishDefaults, + }, + "git+https://user:password@github.com/foo/bar": { ...defaults.github, default: "https", auth: "user:password" }, + [`git+https://user:password@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user:password", + ...committishDefaults, + }, + "git+https://:password@github.com/foo/bar": { ...defaults.github, default: "https", auth: ":password" }, + [`git+https://:password@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: ":password", + ...committishDefaults, + }, + + "git+https://github.com/foo/bar.git": { ...defaults.github, default: "https" }, + [`git+https://github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + ...committishDefaults, + }, + "git+https://user@github.com/foo/bar.git": { ...defaults.github, default: "https", auth: "user" }, + [`git+https://user@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user", + ...committishDefaults, + }, + "git+https://user:password@github.com/foo/bar.git": { + ...defaults.github, + default: "https", + auth: "user:password", + }, + [`git+https://user:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user:password", + ...committishDefaults, + }, + "git+https://:password@github.com/foo/bar.git": { ...defaults.github, default: "https", auth: ":password" }, + [`git+https://:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: ":password", + ...committishDefaults, + }, + + // https urls + // + // NOTE auth is accepted and respected + "https://github.com/foo/bar": { ...defaults.github, default: "https" }, + [`https://github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + ...committishDefaults, + }, + "https://user@github.com/foo/bar": { ...defaults.github, default: "https", auth: "user" }, + [`https://user@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user", + ...committishDefaults, + }, + "https://user:password@github.com/foo/bar": { ...defaults.github, default: "https", auth: "user:password" }, + [`https://user:password@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user:password", + ...committishDefaults, + }, + "https://:password@github.com/foo/bar": { ...defaults.github, default: "https", auth: ":password" }, + [`https://:password@github.com/foo/bar#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: ":password", + ...committishDefaults, + }, + + "https://github.com/foo/bar.git": { ...defaults.github, default: "https" }, + [`https://github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + ...committishDefaults, + }, + "https://user@github.com/foo/bar.git": { ...defaults.github, default: "https", auth: "user" }, + [`https://user@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user", + ...committishDefaults, + }, + "https://user:password@github.com/foo/bar.git": { ...defaults.github, default: "https", auth: "user:password" }, + [`https://user:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: "user:password", + ...committishDefaults, + }, + "https://:password@github.com/foo/bar.git": { ...defaults.github, default: "https", auth: ":password" }, + [`https://:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { + ...defaults.github, + default: "https", + auth: ":password", + ...committishDefaults, + }, + + // inputs that are not quite proper but we accept anyway + "https://www.github.com/foo/bar": { ...defaults.github, default: "https" }, + "foo/bar#branch with space": { ...defaults.github, default: "shortcut", committish: "branch with space" }, + "foo/bar#branch:with:colons": { ...defaults.github, default: "shortcut", committish: "branch:with:colons" }, + "https://github.com/foo/bar/tree/branch": { ...defaults.github, default: "https", committish: "branch" }, + "user..blerg--/..foo-js# . . . . . some . tags / / /": { + ...defaults.github, + default: "shortcut", + user: "user..blerg--", + project: "..foo-js", + committish: " . . . . . some . tags / / /", + }, + }, + gitlab: { + // shortcuts + // + // NOTE auth is accepted but ignored + // NOTE gitlabSubgroups are respected, but the gitlabSubgroup is treated as the project and the real project is lost + "gitlab:foo/bar": { ...defaults.gitlab, default: "shortcut" }, + "gitlab:foo/bar#branch": { ...defaults.gitlab, default: "shortcut", committish: "branch" }, + "gitlab:user@foo/bar": { ...defaults.gitlab, default: "shortcut", auth: null }, + "gitlab:user@foo/bar#branch": { ...defaults.gitlab, default: "shortcut", auth: null, committish: "branch" }, + "gitlab:user:password@foo/bar": { ...defaults.gitlab, default: "shortcut", auth: null }, + "gitlab:user:password@foo/bar#branch": { + ...defaults.gitlab, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gitlab::password@foo/bar": { ...defaults.gitlab, default: "shortcut", auth: null }, + "gitlab::password@foo/bar#branch": { ...defaults.gitlab, default: "shortcut", auth: null, committish: "branch" }, + + "gitlab:foo/bar.git": { ...defaults.gitlab, default: "shortcut" }, + "gitlab:foo/bar.git#branch": { ...defaults.gitlab, default: "shortcut", committish: "branch" }, + "gitlab:user@foo/bar.git": { ...defaults.gitlab, default: "shortcut", auth: null }, + "gitlab:user@foo/bar.git#branch": { ...defaults.gitlab, default: "shortcut", auth: null, committish: "branch" }, + "gitlab:user:password@foo/bar.git": { ...defaults.gitlab, default: "shortcut", auth: null }, + "gitlab:user:password@foo/bar.git#branch": { + ...defaults.gitlab, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gitlab::password@foo/bar.git": { ...defaults.gitlab, default: "shortcut", auth: null }, + "gitlab::password@foo/bar.git#branch": { + ...defaults.gitlab, + default: "shortcut", + auth: null, + committish: "branch", + }, + + "gitlab:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "shortcut" }, + "gitlab:foo/bar/baz#branch": { ...defaults.gitlabSubgroup, default: "shortcut", committish: "branch" }, + "gitlab:user@foo/bar/baz": { ...defaults.gitlabSubgroup, default: "shortcut", auth: null }, + "gitlab:user@foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gitlab:user:password@foo/bar/baz": { ...defaults.gitlabSubgroup, default: "shortcut", auth: null }, + "gitlab:user:password@foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gitlab::password@foo/bar/baz": { ...defaults.gitlabSubgroup, default: "shortcut", auth: null }, + "gitlab::password@foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "shortcut", + auth: null, + committish: "branch", + }, + + "gitlab:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "shortcut" }, + "gitlab:foo/bar/baz.git#branch": { ...defaults.gitlabSubgroup, default: "shortcut", committish: "branch" }, + "gitlab:user@foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "shortcut", auth: null }, + "gitlab:user@foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gitlab:user:password@foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "shortcut", auth: null }, + "gitlab:user:password@foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "shortcut", + auth: null, + committish: "branch", + }, + "gitlab::password@foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "shortcut", auth: null }, + "gitlab::password@foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "shortcut", + auth: null, + committish: "branch", + }, + + // no-protocol git+ssh + // + // NOTE auth is _required_ (see invalid list) but ignored + "user@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "user@gitlab.com:foo/bar#branch": { ...defaults.gitlab, default: "sshurl", auth: null, committish: "branch" }, + "user:password@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "user:password@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + ":password@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "user@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "user@gitlab.com:foo/bar.git#branch": { ...defaults.gitlab, default: "sshurl", auth: null, committish: "branch" }, + "user:password@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "user:password@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + ":password@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "user@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "user@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "user:password@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "user:password@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + ":password@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "user@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "user@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "user:password@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "user:password@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + ":password@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + ":password@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // git+ssh urls + // + // NOTE auth is accepted but ignored + // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost + "git+ssh://gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl" }, + "git+ssh://gitlab.com:foo/bar#branch": { ...defaults.gitlab, default: "sshurl", committish: "branch" }, + "git+ssh://user@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "git+ssh://user@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "git+ssh://user:password@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "git+ssh://:password@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git+ssh://gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl" }, + "git+ssh://gitlab.com:foo/bar.git#branch": { ...defaults.gitlab, default: "sshurl", committish: "branch" }, + "git+ssh://user@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "git+ssh://user@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "git+ssh://user:password@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "git+ssh://:password@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git+ssh://gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl" }, + "git+ssh://gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + committish: "branch", + }, + "git+ssh://user@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "git+ssh://user@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "git+ssh://user:password@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "git+ssh://:password@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "git+ssh://gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl" }, + "git+ssh://gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + committish: "branch", + }, + "git+ssh://user@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "git+ssh://user@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://user:password@gitlab.com:foo/bar/baz.git": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + }, + "git+ssh://user:password@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "git+ssh://:password@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "git+ssh://:password@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // ssh urls + // + // NOTE auth is accepted but ignored + // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost + "ssh://gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl" }, + "ssh://gitlab.com:foo/bar#branch": { ...defaults.gitlab, default: "sshurl", committish: "branch" }, + "ssh://user@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "ssh://user@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "ssh://user:password@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@gitlab.com:foo/bar": { ...defaults.gitlab, default: "sshurl", auth: null }, + "ssh://:password@gitlab.com:foo/bar#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "ssh://gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl" }, + "ssh://gitlab.com:foo/bar.git#branch": { ...defaults.gitlab, default: "sshurl", committish: "branch" }, + "ssh://user@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "ssh://user@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "ssh://user:password@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@gitlab.com:foo/bar.git": { ...defaults.gitlab, default: "sshurl", auth: null }, + "ssh://:password@gitlab.com:foo/bar.git#branch": { + ...defaults.gitlab, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "ssh://gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl" }, + "ssh://gitlab.com:foo/bar/baz#branch": { ...defaults.gitlabSubgroup, default: "sshurl", committish: "branch" }, + "ssh://user@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "ssh://user@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "ssh://user:password@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@gitlab.com:foo/bar/baz": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "ssh://:password@gitlab.com:foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + + "ssh://gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl" }, + "ssh://gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + committish: "branch", + }, + "ssh://user@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "ssh://user@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://user:password@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "ssh://user:password@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + "ssh://:password@gitlab.com:foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "sshurl", auth: null }, + "ssh://:password@gitlab.com:foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // git+https urls + // + // NOTE auth is accepted and respected + // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost + "git+https://gitlab.com/foo/bar": { ...defaults.gitlab, default: "https" }, + "git+https://gitlab.com/foo/bar#branch": { ...defaults.gitlab, default: "https", committish: "branch" }, + "git+https://user@gitlab.com/foo/bar": { ...defaults.gitlab, default: "https", auth: "user" }, + "git+https://user@gitlab.com/foo/bar#branch": { + ...defaults.gitlab, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@gitlab.com/foo/bar": { ...defaults.gitlab, default: "https", auth: "user:password" }, + "git+https://user:password@gitlab.com/foo/bar#branch": { + ...defaults.gitlab, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@gitlab.com/foo/bar": { ...defaults.gitlab, default: "https", auth: ":password" }, + "git+https://:password@gitlab.com/foo/bar#branch": { + ...defaults.gitlab, + default: "https", + auth: ":password", + committish: "branch", + }, + + "git+https://gitlab.com/foo/bar.git": { ...defaults.gitlab, default: "https" }, + "git+https://gitlab.com/foo/bar.git#branch": { ...defaults.gitlab, default: "https", committish: "branch" }, + "git+https://user@gitlab.com/foo/bar.git": { ...defaults.gitlab, default: "https", auth: "user" }, + "git+https://user@gitlab.com/foo/bar.git#branch": { + ...defaults.gitlab, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@gitlab.com/foo/bar.git": { + ...defaults.gitlab, + default: "https", + auth: "user:password", + }, + "git+https://user:password@gitlab.com/foo/bar.git#branch": { + ...defaults.gitlab, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@gitlab.com/foo/bar.git": { ...defaults.gitlab, default: "https", auth: ":password" }, + "git+https://:password@gitlab.com/foo/bar.git#branch": { + ...defaults.gitlab, + default: "https", + auth: ":password", + committish: "branch", + }, + + "git+https://gitlab.com/foo/bar/baz": { ...defaults.gitlabSubgroup, default: "https" }, + "git+https://gitlab.com/foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "https", + committish: "branch", + }, + "git+https://user@gitlab.com/foo/bar/baz": { ...defaults.gitlabSubgroup, default: "https", auth: "user" }, + "git+https://user@gitlab.com/foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@gitlab.com/foo/bar/baz": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + }, + "git+https://user:password@gitlab.com/foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@gitlab.com/foo/bar/baz": { + ...defaults.gitlabSubgroup, + default: "https", + auth: ":password", + }, + "git+https://:password@gitlab.com/foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: ":password", + committish: "branch", + }, + + "git+https://gitlab.com/foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "https" }, + "git+https://gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + committish: "branch", + }, + "git+https://user@gitlab.com/foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "https", auth: "user" }, + "git+https://user@gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user", + committish: "branch", + }, + "git+https://user:password@gitlab.com/foo/bar/baz.git": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + }, + "git+https://user:password@gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + committish: "branch", + }, + "git+https://:password@gitlab.com/foo/bar/baz.git": { + ...defaults.gitlabSubgroup, + default: "https", + auth: ":password", + }, + "git+https://:password@gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: ":password", + committish: "branch", + }, + + // https urls + // + // NOTE auth is accepted and respected + // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost + "https://gitlab.com/foo/bar": { ...defaults.gitlab, default: "https" }, + "https://gitlab.com/foo/bar#branch": { ...defaults.gitlab, default: "https", committish: "branch" }, + "https://user@gitlab.com/foo/bar": { ...defaults.gitlab, default: "https", auth: "user" }, + "https://user@gitlab.com/foo/bar#branch": { + ...defaults.gitlab, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@gitlab.com/foo/bar": { ...defaults.gitlab, default: "https", auth: "user:password" }, + "https://user:password@gitlab.com/foo/bar#branch": { + ...defaults.gitlab, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@gitlab.com/foo/bar": { ...defaults.gitlab, default: "https", auth: ":password" }, + "https://:password@gitlab.com/foo/bar#branch": { + ...defaults.gitlab, + default: "https", + auth: ":password", + committish: "branch", + }, + + "https://gitlab.com/foo/bar.git": { ...defaults.gitlab, default: "https" }, + "https://gitlab.com/foo/bar.git#branch": { ...defaults.gitlab, default: "https", committish: "branch" }, + "https://user@gitlab.com/foo/bar.git": { ...defaults.gitlab, default: "https", auth: "user" }, + "https://user@gitlab.com/foo/bar.git#branch": { + ...defaults.gitlab, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@gitlab.com/foo/bar.git": { ...defaults.gitlab, default: "https", auth: "user:password" }, + "https://user:password@gitlab.com/foo/bar.git#branch": { + ...defaults.gitlab, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@gitlab.com/foo/bar.git": { ...defaults.gitlab, default: "https", auth: ":password" }, + "https://:password@gitlab.com/foo/bar.git#branch": { + ...defaults.gitlab, + default: "https", + auth: ":password", + committish: "branch", + }, + + "https://gitlab.com/foo/bar/baz": { ...defaults.gitlabSubgroup, default: "https" }, + "https://gitlab.com/foo/bar/baz#branch": { ...defaults.gitlabSubgroup, default: "https", committish: "branch" }, + "https://user@gitlab.com/foo/bar/baz": { ...defaults.gitlabSubgroup, default: "https", auth: "user" }, + "https://user@gitlab.com/foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@gitlab.com/foo/bar/baz": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + }, + "https://user:password@gitlab.com/foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@gitlab.com/foo/bar/baz": { ...defaults.gitlabSubgroup, default: "https", auth: ":password" }, + "https://:password@gitlab.com/foo/bar/baz#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: ":password", + committish: "branch", + }, + + "https://gitlab.com/foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "https" }, + "https://gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + committish: "branch", + }, + "https://user@gitlab.com/foo/bar/baz.git": { ...defaults.gitlabSubgroup, default: "https", auth: "user" }, + "https://user@gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user", + committish: "branch", + }, + "https://user:password@gitlab.com/foo/bar/baz.git": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + }, + "https://user:password@gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: "user:password", + committish: "branch", + }, + "https://:password@gitlab.com/foo/bar/baz.git": { + ...defaults.gitlabSubgroup, + default: "https", + auth: ":password", + }, + "https://:password@gitlab.com/foo/bar/baz.git#branch": { + ...defaults.gitlabSubgroup, + default: "https", + auth: ":password", + committish: "branch", + }, + }, + misc: {}, + sourcehut: { + // shortucts + "sourcehut:~foo/bar": { ...defaults.sourcehut, default: "shortcut" }, + "sourcehut:~foo/bar#branch": { ...defaults.sourcehut, default: "shortcut", committish: "branch" }, + + // shortcuts (.git) + "sourcehut:~foo/bar.git": { ...defaults.sourcehut, default: "shortcut" }, + "sourcehut:~foo/bar.git#branch": { ...defaults.sourcehut, default: "shortcut", committish: "branch" }, + + // no-protocol git+ssh + "git@git.sr.ht:~foo/bar": { ...defaults.sourcehut, default: "sshurl", auth: null }, + "git@git.sr.ht:~foo/bar#branch": { + ...defaults.sourcehut, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // no-protocol git+ssh (.git) + "git@git.sr.ht:~foo/bar.git": { ...defaults.sourcehut, default: "sshurl", auth: null }, + "git@git.sr.ht:~foo/bar.git#branch": { + ...defaults.sourcehut, + default: "sshurl", + auth: null, + committish: "branch", + }, + + // git+ssh urls + "git+ssh://git@git.sr.ht:~foo/bar": { ...defaults.sourcehut, default: "sshurl" }, + "git+ssh://git@git.sr.ht:~foo/bar#branch": { + ...defaults.sourcehut, + default: "sshurl", + committish: "branch", + }, + + // git+ssh urls (.git) + "git+ssh://git@git.sr.ht:~foo/bar.git": { ...defaults.sourcehut, default: "sshurl" }, + "git+ssh://git@git.sr.ht:~foo/bar.git#branch": { + ...defaults.sourcehut, + default: "sshurl", + committish: "branch", + }, + + // https urls + "https://git.sr.ht/~foo/bar": { ...defaults.sourcehut, default: "https" }, + "https://git.sr.ht/~foo/bar#branch": { ...defaults.sourcehut, default: "https", committish: "branch" }, + + "https://git.sr.ht/~foo/bar.git": { ...defaults.sourcehut, default: "https" }, + "https://git.sr.ht/~foo/bar.git#branch": { ...defaults.sourcehut, default: "https", committish: "branch" }, + }, +}; + +export const invalidGitUrls = { + bitbucket: [ + // invalid protocol + "git://bitbucket.org/foo/bar", + // url to get a tarball + "https://bitbucket.org/foo/bar/get/archive.tar.gz", + // missing project + "https://bitbucket.org/foo", + ], + gist: [ + // raw urls that are wrong anyway but for some reason are in the wild + "https://gist.github.com/foo/feedbeef/raw/fix%2Fbug/", + // missing both user and project + "https://gist.github.com/", + ], + github: [ + // foo/bar shorthand but specifying auth + "user@foo/bar", + "user:password@foo/bar", + ":password@foo/bar", + // foo/bar shorthand but with a space in it + "foo/ bar", + // string that ends with a slash, probably a directory + "foo/bar/", + // git@github.com style, but omitting the username + "github.com:foo/bar", + "github.com/foo/bar", + // invalid URI encoding + "github:foo%0N/bar", + // missing path + "git+ssh://git@github.com:", + // a deep url to something we don't know + "https://github.com/foo/bar/issues", + ], + gitlab: [ + // gitlab urls can contain a /-/ segment, make sure we ignore those + "https://gitlab.com/foo/-/something", + // missing project + "https://gitlab.com/foo", + // tarball, this should not parse so that it can be used for pacote's remote fetcher + "https://gitlab.com/foo/bar/repository/archive.tar.gz", + "https://gitlab.com/foo/bar/repository/archive.tar.gz?ref=49b393e2ded775f2df36ef2ffcb61b0359c194c9", + ], + misc: [ + "https://google.com", + "git+ssh://git@nothosted.com/abc/def", + "git://nothosted.com", + "git+file:///foo/bar", + "git+ssh://git@git.unlucky.com:RND/electron-tools/some-tool#2.0.1", + "::", + "", + null, + undefined, + ], + sourcehut: [ + // missing project + "https://git.sr.ht/~foo", + // invalid protocols + "git://git@git.sr.ht:~foo/bar", + "ssh://git.sr.ht:~foo/bar", + // tarball url + "https://git.sr.ht/~foo/bar/archive/HEAD.tar.gz", + ], +}; diff --git a/test/cli/install/hosted-git-info/from-url.test.ts b/test/cli/install/hosted-git-info/from-url.test.ts new file mode 100644 index 0000000000..e6cb2ab12c --- /dev/null +++ b/test/cli/install/hosted-git-info/from-url.test.ts @@ -0,0 +1,31 @@ +import { hostedGitInfo } from "bun:internal-for-testing"; +import { describe, expect, it } from "bun:test"; +import { invalidGitUrls, validGitUrls } from "./cases"; + +describe("fromUrl", () => { + describe("valid urls", () => { + describe.each(Object.entries(validGitUrls))("%s", (_, urlset: object) => { + it.each(Object.entries(urlset))("parses %s", (url, expected) => { + expect(hostedGitInfo.fromUrl(url)).toMatchObject({ + ...(expected.type && { type: expected.type }), + ...(expected.domain && { domain: expected.domain }), + ...(expected.user && { user: expected.user }), + ...(expected.project && { project: expected.project }), + ...(expected.committish && { committish: expected.committish }), + ...(expected.default && { default: expected.default }), + }); + }); + }); + }); + + // TODO(markovejnovic): Unskip these tests. + describe.skip("invalid urls", () => { + describe.each(Object.entries(invalidGitUrls))("%s", (_, urls: (string | null | undefined)[]) => { + it.each(urls)("does not permit %s", url => { + expect(() => { + hostedGitInfo.fromUrl(url); + }).toThrow(); + }); + }); + }); +}); diff --git a/test/cli/install/hosted-git-info/parse-url.test.ts b/test/cli/install/hosted-git-info/parse-url.test.ts new file mode 100644 index 0000000000..c88019b66a --- /dev/null +++ b/test/cli/install/hosted-git-info/parse-url.test.ts @@ -0,0 +1,21 @@ +/** + * Mimics https://github.com/npm/hosted-git-info/blob/main/test/parse-url.js + */ +import { hostedGitInfo } from "bun:internal-for-testing"; +import { describe, expect, it } from "bun:test"; + +const okCases = [ + // These come straight out of the hosted-git-info tests + "git+ssh://git@abc:frontend/utils.git#6d45447e0c5eb6cd2e3edf05a8c5a9bb81950c79", + // These are custom cases added for Bun + "ssh://:password@bitbucket.org:foo/bar.git", + "git@bitbucket.org:foo/bar", + "gist:user:password@/feedbeef#branch", + "github:foo/bar#branch with space", +]; + +describe("parseUrl", () => { + it.each(okCases)("parses %s", url => { + expect(hostedGitInfo.parseUrl(url)).not.toBeNull(); + }); +});