1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-06-29 10:31:15 +02:00

Merge pull request #12164 from NixOS/mergify/bp/2.25-maintenance/pr-12157

parsePathFlakeRefWithFragment(): Handle 'path?query' without a fragment (backport #12157)
This commit is contained in:
Eelco Dolstra 2025-01-10 18:11:30 +01:00 committed by GitHub
commit c902a299a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 126 additions and 109 deletions

View file

@ -66,7 +66,7 @@ Input Input::fromURL(
} }
} }
throw Error("input '%s' is unsupported", url.url); throw Error("input '%s' is unsupported", url);
} }
Input Input::fromAttrs(const Settings & settings, Attrs && attrs) Input Input::fromAttrs(const Settings & settings, Attrs && attrs)

View file

@ -425,7 +425,7 @@ struct GitInputScheme : InputScheme
auto url = parseURL(getStrAttr(input.attrs, "url")); auto url = parseURL(getStrAttr(input.attrs, "url"));
bool isBareRepository = url.scheme == "file" && !pathExists(url.path + "/.git"); bool isBareRepository = url.scheme == "file" && !pathExists(url.path + "/.git");
repoInfo.isLocal = url.scheme == "file" && !forceHttp && !isBareRepository; repoInfo.isLocal = url.scheme == "file" && !forceHttp && !isBareRepository;
repoInfo.url = repoInfo.isLocal ? url.path : url.base; repoInfo.url = repoInfo.isLocal ? url.path : url.to_string();
// If this is a local directory and no ref or revision is // If this is a local directory and no ref or revision is
// given, then allow the use of an unclean working tree. // given, then allow the use of an unclean working tree.

View file

@ -50,7 +50,7 @@ struct GitArchiveInputScheme : InputScheme
else if (std::regex_match(path[2], refRegex)) else if (std::regex_match(path[2], refRegex))
ref = path[2]; ref = path[2];
else else
throw BadURL("in URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[2]); throw BadURL("in URL '%s', '%s' is not a commit hash or branch/tag name", url, path[2]);
} else if (size > 3) { } else if (size > 3) {
std::string rs; std::string rs;
for (auto i = std::next(path.begin(), 2); i != path.end(); i++) { for (auto i = std::next(path.begin(), 2); i != path.end(); i++) {
@ -63,34 +63,34 @@ struct GitArchiveInputScheme : InputScheme
if (std::regex_match(rs, refRegex)) { if (std::regex_match(rs, refRegex)) {
ref = rs; ref = rs;
} else { } else {
throw BadURL("in URL '%s', '%s' is not a branch/tag name", url.url, rs); throw BadURL("in URL '%s', '%s' is not a branch/tag name", url, rs);
} }
} else if (size < 2) } else if (size < 2)
throw BadURL("URL '%s' is invalid", url.url); throw BadURL("URL '%s' is invalid", url);
for (auto &[name, value] : url.query) { for (auto &[name, value] : url.query) {
if (name == "rev") { if (name == "rev") {
if (rev) if (rev)
throw BadURL("URL '%s' contains multiple commit hashes", url.url); throw BadURL("URL '%s' contains multiple commit hashes", url);
rev = Hash::parseAny(value, HashAlgorithm::SHA1); rev = Hash::parseAny(value, HashAlgorithm::SHA1);
} }
else if (name == "ref") { else if (name == "ref") {
if (!std::regex_match(value, refRegex)) if (!std::regex_match(value, refRegex))
throw BadURL("URL '%s' contains an invalid branch/tag name", url.url); throw BadURL("URL '%s' contains an invalid branch/tag name", url);
if (ref) if (ref)
throw BadURL("URL '%s' contains multiple branch/tag names", url.url); throw BadURL("URL '%s' contains multiple branch/tag names", url);
ref = value; ref = value;
} }
else if (name == "host") { else if (name == "host") {
if (!std::regex_match(value, hostRegex)) if (!std::regex_match(value, hostRegex))
throw BadURL("URL '%s' contains an invalid instance host", url.url); throw BadURL("URL '%s' contains an invalid instance host", url);
host_url = value; host_url = value;
} }
// FIXME: barf on unsupported attributes // FIXME: barf on unsupported attributes
} }
if (ref && rev) if (ref && rev)
throw BadURL("URL '%s' contains both a commit hash and a branch/tag name %s %s", url.url, *ref, rev->gitRev()); throw BadURL("URL '%s' contains both a commit hash and a branch/tag name %s %s", url, *ref, rev->gitRev());
Input input{settings}; Input input{settings};
input.attrs.insert_or_assign("type", std::string { schemeName() }); input.attrs.insert_or_assign("type", std::string { schemeName() });

View file

@ -26,16 +26,16 @@ struct IndirectInputScheme : InputScheme
else if (std::regex_match(path[1], refRegex)) else if (std::regex_match(path[1], refRegex))
ref = path[1]; ref = path[1];
else else
throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[1]); throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url, path[1]);
} else if (path.size() == 3) { } else if (path.size() == 3) {
if (!std::regex_match(path[1], refRegex)) if (!std::regex_match(path[1], refRegex))
throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url.url, path[1]); throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url, path[1]);
ref = path[1]; ref = path[1];
if (!std::regex_match(path[2], revRegex)) if (!std::regex_match(path[2], revRegex))
throw BadURL("in flake URL '%s', '%s' is not a commit hash", url.url, path[2]); throw BadURL("in flake URL '%s', '%s' is not a commit hash", url, path[2]);
rev = Hash::parseAny(path[2], HashAlgorithm::SHA1); rev = Hash::parseAny(path[2], HashAlgorithm::SHA1);
} else } else
throw BadURL("GitHub URL '%s' is invalid", url.url); throw BadURL("GitHub URL '%s' is invalid", url);
std::string id = path[0]; std::string id = path[0];
if (!std::regex_match(id, flakeRegex)) if (!std::regex_match(id, flakeRegex))

View file

@ -161,7 +161,7 @@ struct MercurialInputScheme : InputScheme
{ {
auto url = parseURL(getStrAttr(input.attrs, "url")); auto url = parseURL(getStrAttr(input.attrs, "url"));
bool isLocal = url.scheme == "file"; bool isLocal = url.scheme == "file";
return {isLocal, isLocal ? url.path : url.base}; return {isLocal, isLocal ? url.path : url.to_string()};
} }
StorePath fetchToStore(ref<Store> store, Input & input) const StorePath fetchToStore(ref<Store> store, Input & input) const

View file

@ -14,7 +14,7 @@ struct PathInputScheme : InputScheme
if (url.scheme != "path") return {}; if (url.scheme != "path") return {};
if (url.authority && *url.authority != "") if (url.authority && *url.authority != "")
throw Error("path URL '%s' should not have an authority ('%s')", url.url, *url.authority); throw Error("path URL '%s' should not have an authority ('%s')", url, *url.authority);
Input input{settings}; Input input{settings};
input.attrs.insert_or_assign("type", "path"); input.attrs.insert_or_assign("type", "path");
@ -27,10 +27,10 @@ struct PathInputScheme : InputScheme
if (auto n = string2Int<uint64_t>(value)) if (auto n = string2Int<uint64_t>(value))
input.attrs.insert_or_assign(name, *n); input.attrs.insert_or_assign(name, *n);
else else
throw Error("path URL '%s' has invalid parameter '%s'", url.to_string(), name); throw Error("path URL '%s' has invalid parameter '%s'", url, name);
} }
else else
throw Error("path URL '%s' has unsupported parameter '%s'", url.to_string(), name); throw Error("path URL '%s' has unsupported parameter '%s'", url, name);
return input; return input;
} }

View file

@ -7,18 +7,60 @@ namespace nix {
/* ----------- tests for flake/flakeref.hh --------------------------------------------------*/ /* ----------- tests for flake/flakeref.hh --------------------------------------------------*/
/* ---------------------------------------------------------------------------- TEST(parseFlakeRef, path) {
* to_string experimentalFeatureSettings.experimentalFeatures.get().insert(Xp::Flakes);
* --------------------------------------------------------------------------*/
fetchers::Settings fetchSettings;
{
auto s = "/foo/bar";
auto flakeref = parseFlakeRef(fetchSettings, s);
ASSERT_EQ(flakeref.to_string(), "path:/foo/bar");
}
{
auto s = "/foo/bar?revCount=123&rev=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
auto flakeref = parseFlakeRef(fetchSettings, s);
ASSERT_EQ(flakeref.to_string(), "path:/foo/bar?rev=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&revCount=123");
}
{
auto s = "/foo/bar?xyzzy=123";
EXPECT_THROW(
parseFlakeRef(fetchSettings, s),
Error);
}
{
auto s = "/foo/bar#bla";
EXPECT_THROW(
parseFlakeRef(fetchSettings, s),
Error);
}
{
auto s = "/foo/bar#bla";
auto [flakeref, fragment] = parseFlakeRefWithFragment(fetchSettings, s);
ASSERT_EQ(flakeref.to_string(), "path:/foo/bar");
ASSERT_EQ(fragment, "bla");
}
{
auto s = "/foo/bar?revCount=123#bla";
auto [flakeref, fragment] = parseFlakeRefWithFragment(fetchSettings, s);
ASSERT_EQ(flakeref.to_string(), "path:/foo/bar?revCount=123");
ASSERT_EQ(fragment, "bla");
}
}
TEST(to_string, doesntReencodeUrl) { TEST(to_string, doesntReencodeUrl) {
fetchers::Settings fetchSettings; fetchers::Settings fetchSettings;
auto s = "http://localhost:8181/test/+3d.tar.gz"; auto s = "http://localhost:8181/test/+3d.tar.gz";
auto flakeref = parseFlakeRef(fetchSettings, s); auto flakeref = parseFlakeRef(fetchSettings, s);
auto parsed = flakeref.to_string(); auto unparsed = flakeref.to_string();
auto expected = "http://localhost:8181/test/%2B3d.tar.gz"; auto expected = "http://localhost:8181/test/%2B3d.tar.gz";
ASSERT_EQ(parsed, expected); ASSERT_EQ(unparsed, expected);
} }
} }

View file

@ -67,6 +67,20 @@ std::optional<FlakeRef> maybeParseFlakeRef(
} }
} }
static std::pair<FlakeRef, std::string> fromParsedURL(
const fetchers::Settings & fetchSettings,
ParsedURL && parsedURL,
bool isFlake)
{
auto dir = getOr(parsedURL.query, "dir", "");
parsedURL.query.erase("dir");
std::string fragment;
std::swap(fragment, parsedURL.fragment);
return {FlakeRef(fetchers::Input::fromURL(fetchSettings, parsedURL, isFlake), dir), fragment};
}
std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment( std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
const fetchers::Settings & fetchSettings, const fetchers::Settings & fetchSettings,
const std::string & url, const std::string & url,
@ -74,23 +88,16 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
bool allowMissing, bool allowMissing,
bool isFlake) bool isFlake)
{ {
std::string path = url; static std::regex pathFlakeRegex(
std::string fragment = ""; R"(([^?#]*)(\?([^#]*))?(#(.*))?)",
std::map<std::string, std::string> query; std::regex::ECMAScript);
auto pathEnd = url.find_first_of("#?");
auto fragmentStart = pathEnd; std::smatch match;
if (pathEnd != std::string::npos && url[pathEnd] == '?') { auto succeeds = std::regex_match(url, match, pathFlakeRegex);
fragmentStart = url.find("#"); assert(succeeds);
} auto path = match[1].str();
if (pathEnd != std::string::npos) { auto query = decodeQuery(match[3]);
path = url.substr(0, pathEnd); auto fragment = percentDecode(match[5].str());
}
if (fragmentStart != std::string::npos) {
fragment = percentDecode(url.substr(fragmentStart+1));
}
if (pathEnd != std::string::npos && fragmentStart != std::string::npos && url[pathEnd] == '?') {
query = decodeQuery(url.substr(pathEnd+1, fragmentStart-pathEnd-1));
}
if (baseDir) { if (baseDir) {
/* Check if 'url' is a path (either absolute or relative /* Check if 'url' is a path (either absolute or relative
@ -144,15 +151,12 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
while (flakeRoot != "/") { while (flakeRoot != "/") {
if (pathExists(flakeRoot + "/.git")) { if (pathExists(flakeRoot + "/.git")) {
auto base = std::string("git+file://") + flakeRoot;
auto parsedURL = ParsedURL{ auto parsedURL = ParsedURL{
.url = base, // FIXME
.base = base,
.scheme = "git+file", .scheme = "git+file",
.authority = "", .authority = "",
.path = flakeRoot, .path = flakeRoot,
.query = query, .query = query,
.fragment = fragment,
}; };
if (subdir != "") { if (subdir != "") {
@ -164,9 +168,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
if (pathExists(flakeRoot + "/.git/shallow")) if (pathExists(flakeRoot + "/.git/shallow"))
parsedURL.query.insert_or_assign("shallow", "1"); parsedURL.query.insert_or_assign("shallow", "1");
return std::make_pair( return fromParsedURL(fetchSettings, std::move(parsedURL), isFlake);
FlakeRef(fetchers::Input::fromURL(fetchSettings, parsedURL), getOr(parsedURL.query, "dir", "")),
fragment);
} }
subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir); subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir);
@ -180,16 +182,19 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
path = canonPath(path + "/" + getOr(query, "dir", "")); path = canonPath(path + "/" + getOr(query, "dir", ""));
} }
fetchers::Attrs attrs; return fromParsedURL(fetchSettings, {
attrs.insert_or_assign("type", "path"); .scheme = "path",
attrs.insert_or_assign("path", path); .authority = "",
.path = path,
.query = query,
.fragment = fragment
}, isFlake);
}
return std::make_pair(FlakeRef(fetchers::Input::fromAttrs(fetchSettings, std::move(attrs)), ""), fragment); /**
}; * Check if `url` is a flake ID. This is an abbreviated syntax for
* `flake:<flake-id>?ref=<ref>&rev=<rev>`.
*/
/* Check if 'url' is a flake ID. This is an abbreviated syntax for
'flake:<flake-id>?ref=<ref>&rev=<rev>'. */
static std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef( static std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
const fetchers::Settings & fetchSettings, const fetchers::Settings & fetchSettings,
const std::string & url, const std::string & url,
@ -205,8 +210,6 @@ static std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(
if (std::regex_match(url, match, flakeRegex)) { if (std::regex_match(url, match, flakeRegex)) {
auto parsedURL = ParsedURL{ auto parsedURL = ParsedURL{
.url = url,
.base = "flake:" + match.str(1),
.scheme = "flake", .scheme = "flake",
.authority = "", .authority = "",
.path = match[1], .path = match[1],
@ -227,22 +230,11 @@ std::optional<std::pair<FlakeRef, std::string>> parseURLFlakeRef(
bool isFlake bool isFlake
) )
{ {
ParsedURL parsedURL;
try { try {
parsedURL = parseURL(url); return fromParsedURL(fetchSettings, parseURL(url), isFlake);
} catch (BadURL &) { } catch (BadURL &) {
return std::nullopt; return std::nullopt;
} }
std::string fragment;
std::swap(fragment, parsedURL.fragment);
auto input = fetchers::Input::fromURL(fetchSettings, parsedURL, isFlake);
input.parent = baseDir;
return std::make_pair(
FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")),
fragment);
} }
std::pair<FlakeRef, std::string> parseFlakeRefWithFragment( std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(

View file

@ -20,24 +20,11 @@ namespace nix {
} }
std::ostream& operator<<(std::ostream& os, const ParsedURL& p) {
return os << "\n"
<< "url: " << p.url << "\n"
<< "base: " << p.base << "\n"
<< "scheme: " << p.scheme << "\n"
<< "authority: " << p.authority.value() << "\n"
<< "path: " << p.path << "\n"
<< "query: " << print_map(p.query) << "\n"
<< "fragment: " << p.fragment << "\n";
}
TEST(parseURL, parsesSimpleHttpUrl) { TEST(parseURL, parsesSimpleHttpUrl) {
auto s = "http://www.example.org/file.tar.gz"; auto s = "http://www.example.org/file.tar.gz";
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "http://www.example.org/file.tar.gz",
.base = "http://www.example.org/file.tar.gz",
.scheme = "http", .scheme = "http",
.authority = "www.example.org", .authority = "www.example.org",
.path = "/file.tar.gz", .path = "/file.tar.gz",
@ -53,8 +40,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "https://www.example.org/file.tar.gz",
.base = "https://www.example.org/file.tar.gz",
.scheme = "https", .scheme = "https",
.authority = "www.example.org", .authority = "www.example.org",
.path = "/file.tar.gz", .path = "/file.tar.gz",
@ -70,8 +55,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "https://www.example.org/file.tar.gz",
.base = "https://www.example.org/file.tar.gz",
.scheme = "https", .scheme = "https",
.authority = "www.example.org", .authority = "www.example.org",
.path = "/file.tar.gz", .path = "/file.tar.gz",
@ -87,8 +70,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "http://www.example.org/file.tar.gz",
.base = "http://www.example.org/file.tar.gz",
.scheme = "http", .scheme = "http",
.authority = "www.example.org", .authority = "www.example.org",
.path = "/file.tar.gz", .path = "/file.tar.gz",
@ -104,8 +85,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "file+https://www.example.org/video.mp4",
.base = "https://www.example.org/video.mp4",
.scheme = "file+https", .scheme = "file+https",
.authority = "www.example.org", .authority = "www.example.org",
.path = "/video.mp4", .path = "/video.mp4",
@ -126,8 +105,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "http://127.0.0.1:8080/file.tar.gz",
.base = "https://127.0.0.1:8080/file.tar.gz",
.scheme = "http", .scheme = "http",
.authority = "127.0.0.1:8080", .authority = "127.0.0.1:8080",
.path = "/file.tar.gz", .path = "/file.tar.gz",
@ -143,8 +120,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "http://[fe80::818c:da4d:8975:415c\%enp0s25]:8080",
.base = "http://[fe80::818c:da4d:8975:415c\%enp0s25]:8080",
.scheme = "http", .scheme = "http",
.authority = "[fe80::818c:da4d:8975:415c\%enp0s25]:8080", .authority = "[fe80::818c:da4d:8975:415c\%enp0s25]:8080",
.path = "", .path = "",
@ -161,8 +136,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080",
.base = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080",
.scheme = "http", .scheme = "http",
.authority = "[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080", .authority = "[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080",
.path = "", .path = "",
@ -185,8 +158,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "http://user:pass@www.example.org/file.tar.gz",
.base = "http://user:pass@www.example.org/file.tar.gz",
.scheme = "http", .scheme = "http",
.authority = "user:pass@www.example.org:8080", .authority = "user:pass@www.example.org:8080",
.path = "/file.tar.gz", .path = "/file.tar.gz",
@ -203,8 +174,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "",
.base = "",
.scheme = "file", .scheme = "file",
.authority = "", .authority = "",
.path = "/none/of//your/business", .path = "/none/of//your/business",
@ -228,8 +197,6 @@ namespace nix {
auto parsed = parseURL(s); auto parsed = parseURL(s);
ParsedURL expected { ParsedURL expected {
.url = "ftp://ftp.nixos.org/downloads/nixos.iso",
.base = "ftp://ftp.nixos.org/downloads/nixos.iso",
.scheme = "ftp", .scheme = "ftp",
.authority = "ftp.nixos.org", .authority = "ftp.nixos.org",
.path = "/downloads/nixos.iso", .path = "/downloads/nixos.iso",

View file

@ -260,6 +260,7 @@ public:
operator const T &() const { return value; } operator const T &() const { return value; }
operator T &() { return value; } operator T &() { return value; }
const T & get() const { return value; } const T & get() const { return value; }
T & get() { return value; }
template<typename U> template<typename U>
bool operator ==(const U & v2) const { return value == v2; } bool operator ==(const U & v2) const { return value == v2; }
template<typename U> template<typename U>

View file

@ -22,7 +22,6 @@ ParsedURL parseURL(const std::string & url)
std::smatch match; std::smatch match;
if (std::regex_match(url, match, uriRegex)) { if (std::regex_match(url, match, uriRegex)) {
auto & base = match[1];
std::string scheme = match[2]; std::string scheme = match[2];
auto authority = match[3].matched auto authority = match[3].matched
? std::optional<std::string>(match[3]) : std::nullopt; ? std::optional<std::string>(match[3]) : std::nullopt;
@ -40,8 +39,6 @@ ParsedURL parseURL(const std::string & url)
path = "/"; path = "/";
return ParsedURL{ return ParsedURL{
.url = url,
.base = base,
.scheme = scheme, .scheme = scheme,
.authority = authority, .authority = authority,
.path = percentDecode(path), .path = percentDecode(path),
@ -136,6 +133,12 @@ std::string ParsedURL::to_string() const
+ (fragment.empty() ? "" : "#" + percentEncode(fragment)); + (fragment.empty() ? "" : "#" + percentEncode(fragment));
} }
std::ostream & operator << (std::ostream & os, const ParsedURL & url)
{
os << url.to_string();
return os;
}
bool ParsedURL::operator ==(const ParsedURL & other) const noexcept bool ParsedURL::operator ==(const ParsedURL & other) const noexcept
{ {
return return

View file

@ -7,9 +7,6 @@ namespace nix {
struct ParsedURL struct ParsedURL
{ {
std::string url;
/// URL without query/fragment
std::string base;
std::string scheme; std::string scheme;
std::optional<std::string> authority; std::optional<std::string> authority;
std::string path; std::string path;
@ -26,6 +23,8 @@ struct ParsedURL
ParsedURL canonicalise(); ParsedURL canonicalise();
}; };
std::ostream & operator << (std::ostream & os, const ParsedURL & url);
MakeError(BadURL, Error); MakeError(BadURL, Error);
std::string percentDecode(std::string_view in); std::string percentDecode(std::string_view in);

View file

@ -63,3 +63,16 @@ flakeref=git+file://$rootRepo\?submodules=1\&dir=submodule
echo '"foo"' > "$rootRepo"/submodule/sub.nix echo '"foo"' > "$rootRepo"/submodule/sub.nix
[[ $(nix eval --json "$flakeref#sub" ) = '"foo"' ]] [[ $(nix eval --json "$flakeref#sub" ) = '"foo"' ]]
[[ $(nix flake metadata --json "$flakeref" | jq -r .locked.rev) = null ]] [[ $(nix flake metadata --json "$flakeref" | jq -r .locked.rev) = null ]]
# Test that `nix flake metadata` parses `submodule` correctly.
cat > "$rootRepo"/flake.nix <<EOF
{
outputs = { self }: {
};
}
EOF
git -C "$rootRepo" add flake.nix
git -C "$rootRepo" commit -m "Add flake.nix"
storePath=$(nix flake metadata --json "$rootRepo?submodules=1" | jq -r .path)
[[ -e "$storePath/submodule" ]]