diff --git a/src/libfetchers-tests/git-utils.cc b/src/libfetchers-tests/git-utils.cc index 8c306655d6f..c20c494142e 100644 --- a/src/libfetchers-tests/git-utils.cc +++ b/src/libfetchers-tests/git-utils.cc @@ -92,7 +92,7 @@ TEST_F(GitUtilsTest, sink_basic) // sink->createHardlink("foo-1.1/links/foo-2", CanonPath("foo-1.1/hello")); auto result = repo->dereferenceSingletonDirectory(sink->flush()); - auto accessor = repo->getAccessor(result, false, getRepoName()); + auto accessor = repo->getAccessor(result, {}, getRepoName()); auto entries = accessor->readDirectory(CanonPath::root); ASSERT_EQ(entries.size(), 5u); ASSERT_EQ(accessor->readFile(CanonPath("hello")), "hello world"); diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index 34f5deb3384..abd0e294094 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -327,7 +327,12 @@ std::pair, Input> Input::getAccessorUnchecked(const Settings auto makeStoreAccessor = [&]() -> std::pair, Input> { auto accessor = make_ref(ref{store->getFSAccessor(*storePath)}); - accessor->fingerprint = getFingerprint(store); + // FIXME: use the NAR hash for fingerprinting Git trees that have a .gitattributes file, since we don't know if + // we used `git archive` or libgit2 to fetch it. + accessor->fingerprint = getType() == "git" && accessor->pathExists(CanonPath(".gitattributes")) + ? std::optional(storePath->hashPart()) + : getFingerprint(store); + cachedFingerprint = accessor->fingerprint; // Store a cache entry for the substituted tree so later fetches // can reuse the existing nar instead of copying the unpacked @@ -357,10 +362,10 @@ std::pair, Input> Input::getAccessorUnchecked(const Settings try { auto [accessor, result] = scheme->getAccessor(settings, store, *this); - if (!accessor->fingerprint) - accessor->fingerprint = result.getFingerprint(store); + if (auto fp = accessor->getFingerprint(CanonPath::root).second) + result.cachedFingerprint = *fp; else - result.cachedFingerprint = accessor->fingerprint; + accessor->fingerprint = result.getFingerprint(store); return {accessor, std::move(result)}; } catch (Error & e) { diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index cfb396bb416..edaae94e00c 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -550,14 +550,15 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this } /** - * A 'GitSourceAccessor' with no regard for export-ignore or any other transformations. + * A 'GitSourceAccessor' with no regard for export-ignore. */ - ref getRawAccessor(const Hash & rev, bool smudgeLfs = false); + ref getRawAccessor(const Hash & rev, const GitAccessorOptions & options); ref - getAccessor(const Hash & rev, bool exportIgnore, std::string displayPrefix, bool smudgeLfs = false) override; + getAccessor(const Hash & rev, const GitAccessorOptions & options, std::string displayPrefix) override; - ref getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError e) override; + ref + getAccessor(const WorkdirInfo & wd, const GitAccessorOptions & options, MakeNotAllowedError e) override; ref getFileSystemObjectSink() override; @@ -700,7 +701,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this Hash treeHashToNarHash(const fetchers::Settings & settings, const Hash & treeHash) override { - auto accessor = getAccessor(treeHash, false, ""); + auto accessor = getAccessor(treeHash, {}, ""); fetchers::Cache::Key cacheKey{"treeHashToNarHash", {{"treeHash", treeHash.gitRev()}}}; @@ -737,10 +738,14 @@ ref GitRepo::openRepo(const std::filesystem::path & path, bool create, return make_ref(path, create, bare); } +std::string GitAccessorOptions::makeFingerprint(const Hash & rev) const +{ + return "git:" + rev.gitRev() + (exportIgnore ? ";e" : "") + (smudgeLfs ? ";l" : ""); +} + /** * Raw git tree input accessor. */ - struct GitSourceAccessor : SourceAccessor { struct State @@ -748,17 +753,20 @@ struct GitSourceAccessor : SourceAccessor ref repo; Object root; std::optional lfsFetch = std::nullopt; + GitAccessorOptions options; }; Sync state_; - GitSourceAccessor(ref repo_, const Hash & rev, bool smudgeLfs) + GitSourceAccessor(ref repo_, const Hash & rev, const GitAccessorOptions & options) : state_{State{ .repo = repo_, .root = peelToTreeOrBlob(lookupObject(*repo_, hashToOID(rev)).get()), - .lfsFetch = smudgeLfs ? std::make_optional(lfs::Fetch(*repo_, hashToOID(rev))) : std::nullopt, + .lfsFetch = options.smudgeLfs ? std::make_optional(lfs::Fetch(*repo_, hashToOID(rev))) : std::nullopt, + .options = options, }} { + fingerprint = options.makeFingerprint(rev); } std::string readBlob(const CanonPath & path, bool symlink) @@ -1307,26 +1315,26 @@ struct GitFileSystemObjectSinkImpl : GitFileSystemObjectSink } }; -ref GitRepoImpl::getRawAccessor(const Hash & rev, bool smudgeLfs) +ref GitRepoImpl::getRawAccessor(const Hash & rev, const GitAccessorOptions & options) { auto self = ref(shared_from_this()); - return make_ref(self, rev, smudgeLfs); + return make_ref(self, rev, options); } ref -GitRepoImpl::getAccessor(const Hash & rev, bool exportIgnore, std::string displayPrefix, bool smudgeLfs) +GitRepoImpl::getAccessor(const Hash & rev, const GitAccessorOptions & options, std::string displayPrefix) { auto self = ref(shared_from_this()); - ref rawGitAccessor = getRawAccessor(rev, smudgeLfs); + ref rawGitAccessor = getRawAccessor(rev, options); rawGitAccessor->setPathDisplay(std::move(displayPrefix)); - if (exportIgnore) + if (options.exportIgnore) return make_ref(self, rawGitAccessor, rev); else return rawGitAccessor; } -ref -GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError) +ref GitRepoImpl::getAccessor( + const WorkdirInfo & wd, const GitAccessorOptions & options, MakeNotAllowedError makeNotAllowedError) { auto self = ref(shared_from_this()); ref fileAccessor = AllowListSourceAccessor::create( @@ -1336,7 +1344,7 @@ GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllow boost::unordered_flat_set{CanonPath::root}, std::move(makeNotAllowedError)) .cast(); - if (exportIgnore) + if (options.exportIgnore) fileAccessor = make_ref(self, fileAccessor, std::nullopt); return fileAccessor; } @@ -1351,7 +1359,7 @@ std::vector> GitRepoImpl::getSubmodules /* Read the .gitmodules files from this revision. */ CanonPath modulesFile(".gitmodules"); - auto accessor = getAccessor(rev, exportIgnore, ""); + auto accessor = getAccessor(rev, {.exportIgnore = exportIgnore}, ""); if (!accessor->pathExists(modulesFile)) return {}; @@ -1368,7 +1376,7 @@ std::vector> GitRepoImpl::getSubmodules std::vector> result; - auto rawAccessor = getRawAccessor(rev); + auto rawAccessor = getRawAccessor(rev, {}); for (auto & submodule : parseSubmodules(pathTemp)) { /* Filter out .gitmodules entries that don't exist or are not diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index e18049093ac..ef5b19af8fe 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -16,6 +16,7 @@ #include "nix/util/json-utils.hh" #include "nix/util/archive.hh" #include "nix/util/mounted-source-accessor.hh" +#include "nix/fetchers/fetch-to-store.hh" #include #include @@ -637,8 +638,72 @@ struct GitInputScheme : InputScheme return shallow || input.getRevCount().has_value(); } + GitAccessorOptions getGitAccessorOptions(const Input & input) const + { + return GitAccessorOptions{ + .exportIgnore = getExportIgnoreAttr(input), + .smudgeLfs = getLfsAttr(input), + .submodules = getSubmodulesAttr(input), + }; + } + + /** + * Get a `SourceAccessor` for the given Git revision using Nix < 2.20 semantics, i.e. using `git archive` or `git + * checkout`. + */ + ref getLegacyGitAccessor( + Store & store, + RepoInfo & repoInfo, + const std::filesystem::path & repoDir, + const Hash & rev, + GitAccessorOptions & options) const + { + auto tmpDir = createTempDir(); + AutoDelete delTmpDir(tmpDir, true); + + auto storePath = + options.submodules + ? [&]() { + // Nix < 2.20 used `git checkout` for repos with submodules. + runProgram2({.program = "git", .args = {"init", tmpDir}}); + runProgram2({.program = "git", .args = {"-C", tmpDir, "remote", "add", "origin", repoDir}}); + runProgram2({.program = "git", .args = {"-C", tmpDir, "fetch", "origin", rev.gitRev()}}); + runProgram2({.program = "git", .args = {"-C", tmpDir, "checkout", rev.gitRev()}}); + PathFilter filter = [&](const Path & path) { return baseNameOf(path) != ".git"; }; + return store.addToStore( + "source", + {getFSSourceAccessor(), CanonPath(tmpDir)}, + ContentAddressMethod::Raw::NixArchive, + HashAlgorithm::SHA256, + {}, + filter); + }() + : [&]() { + // Nix < 2.20 used `git archive` for repos without submodules. + options.exportIgnore = true; + + auto source = sinkToSource([&](Sink & sink) { + runProgram2( + {.program = "git", + .args = {"-C", repoDir, "--git-dir", repoInfo.gitDir, "archive", rev.gitRev()}, + .standardOut = &sink}); + }); + + unpackTarfile(*source, tmpDir); + + return store.addToStore("source", {getFSSourceAccessor(), CanonPath(tmpDir)}); + }(); + + auto accessor = store.getFSAccessor(storePath); + + accessor->fingerprint = options.makeFingerprint(rev) + ";legacy"; + + return ref{accessor}; + } + std::pair, Input> getAccessorFromCommit(const Settings & settings, ref store, RepoInfo & repoInfo, Input && input) const + { assert(!repoInfo.workdirInfo.isDirty); @@ -779,17 +844,59 @@ struct GitInputScheme : InputScheme verifyCommit(input, repo); - bool exportIgnore = getExportIgnoreAttr(input); - bool smudgeLfs = getLfsAttr(input); - auto accessor = repo->getAccessor(rev, exportIgnore, "«" + input.to_string(true) + "»", smudgeLfs); + auto options = getGitAccessorOptions(input); + + auto expectedNarHash = input.getNarHash(); + + auto accessor = repo->getAccessor(rev, options, "«" + input.to_string(true) + "»"); + + if (settings.nix219Compat && !options.smudgeLfs && accessor->pathExists(CanonPath(".gitattributes"))) { + /* Use Nix 2.19 semantics to generate locks, but if a NAR hash is specified, support Nix >= 2.20 semantics + * as well. */ + warn("Using Nix 2.19 semantics to export Git repository '%s'.", input.to_string()); + auto accessorModern = accessor; + accessor = getLegacyGitAccessor(*store, repoInfo, repoDir, rev, options); + if (expectedNarHash) { + auto narHashLegacy = + fetchToStore2(settings, *store, {accessor}, FetchMode::DryRun, input.getName()).second; + if (expectedNarHash != narHashLegacy) { + auto narHashModern = + fetchToStore2(settings, *store, {accessorModern}, FetchMode::DryRun, input.getName()).second; + if (expectedNarHash == narHashModern) + accessor = accessorModern; + } + } + } else { + /* Backward compatibility hack for locks produced by Nix < 2.20 that depend on Nix applying Git filters, + * `export-ignore` or `export-subst`. Nix >= 2.20 doesn't do those, so we may get a NAR hash mismatch. If + * that happens, try again using `git archive`. */ + auto narHashNew = fetchToStore2(settings, *store, {accessor}, FetchMode::DryRun, input.getName()).second; + if (expectedNarHash && accessor->pathExists(CanonPath(".gitattributes"))) { + if (expectedNarHash != narHashNew) { + auto accessorLegacy = getLegacyGitAccessor(*store, repoInfo, repoDir, rev, options); + auto narHashLegacy = + fetchToStore2(settings, *store, {accessorLegacy}, FetchMode::DryRun, input.getName()).second; + if (expectedNarHash == narHashLegacy) { + warn( + "Git input '%s' specifies a NAR hash '%s' that was created by Nix < 2.20.\n" + "Nix >= 2.20 does not apply Git filters, `export-ignore` and `export-subst` by default, which changes the NAR hash.\n" + "Please update the NAR hash to '%s'.", + input.to_string(), + expectedNarHash->to_string(HashFormat::SRI, true), + narHashNew.to_string(HashFormat::SRI, true)); + accessor = accessorLegacy; + } + } + } + } /* If the repo has submodules, fetch them and return a mounted input accessor consisting of the accessor for the top-level repo and the accessors for the submodules. */ - if (getSubmodulesAttr(input)) { + if (options.submodules) { std::map> mounts; - for (auto & [submodule, submoduleRev] : repo->getSubmodules(rev, exportIgnore)) { + for (auto & [submodule, submoduleRev] : repo->getSubmodules(rev, options.exportIgnore)) { auto resolved = repo->resolveSubmoduleUrl(submodule.url); debug( "Git submodule %s: %s %s %s -> %s", @@ -812,9 +919,9 @@ struct GitInputScheme : InputScheme } } attrs.insert_or_assign("rev", submoduleRev.gitRev()); - attrs.insert_or_assign("exportIgnore", Explicit{exportIgnore}); + attrs.insert_or_assign("exportIgnore", Explicit{options.exportIgnore}); attrs.insert_or_assign("submodules", Explicit{true}); - attrs.insert_or_assign("lfs", Explicit{smudgeLfs}); + attrs.insert_or_assign("lfs", Explicit{options.smudgeLfs}); attrs.insert_or_assign("allRefs", Explicit{true}); auto submoduleInput = fetchers::Input::fromAttrs(settings, std::move(attrs)); auto [submoduleAccessor, submoduleInput2] = submoduleInput.getAccessor(settings, store); @@ -823,8 +930,10 @@ struct GitInputScheme : InputScheme } if (!mounts.empty()) { + auto newFingerprint = accessor->getFingerprint(CanonPath::root).second->append(";s"); mounts.insert_or_assign(CanonPath::root, accessor); accessor = makeMountedSourceAccessor(std::move(mounts)); + accessor->fingerprint = newFingerprint; } } @@ -848,7 +957,7 @@ struct GitInputScheme : InputScheme auto exportIgnore = getExportIgnoreAttr(input); ref accessor = - repo->getAccessor(repoInfo.workdirInfo, exportIgnore, makeNotAllowedError(repoPath)); + repo->getAccessor(repoInfo.workdirInfo, {.exportIgnore = exportIgnore}, makeNotAllowedError(repoPath)); /* If the repo has submodules, return a mounted input accessor consisting of the accessor for the top-level repo and the @@ -942,13 +1051,12 @@ struct GitInputScheme : InputScheme std::optional getFingerprint(ref store, const Input & input) const override { - auto makeFingerprint = [&](const Hash & rev) { - return rev.gitRev() + (getSubmodulesAttr(input) ? ";s" : "") + (getExportIgnoreAttr(input) ? ";e" : "") - + (getLfsAttr(input) ? ";l" : ""); - }; + auto options = getGitAccessorOptions(input); if (auto rev = input.getRev()) - return makeFingerprint(*rev); + // FIXME: this can return a wrong fingerprint for the legacy (`git archive`) case, since we don't know here + // whether to append the `;legacy` suffix or not. + return options.makeFingerprint(*rev); else { auto repoInfo = getRepoInfo(input); if (auto repoPath = repoInfo.getPath(); repoPath && repoInfo.workdirInfo.submodules.empty()) { @@ -964,7 +1072,7 @@ struct GitInputScheme : InputScheme writeString("deleted:", hashSink); writeString(file.abs(), hashSink); } - return makeFingerprint(repoInfo.workdirInfo.headRev.value_or(nullRev)) + return options.makeFingerprint(repoInfo.workdirInfo.headRev.value_or(nullRev)) + ";d=" + hashSink.finish().hash.to_string(HashFormat::Base16, false); } return std::nullopt; diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 2f43f71f617..4e418e1ebd9 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -326,7 +326,7 @@ struct GitArchiveInputScheme : InputScheme input.attrs.insert_or_assign("lastModified", uint64_t(tarballInfo.lastModified)); auto accessor = - settings.getTarballCache()->getAccessor(tarballInfo.treeHash, false, "«" + input.to_string(true) + "»"); + settings.getTarballCache()->getAccessor(tarballInfo.treeHash, {}, "«" + input.to_string(true) + "»"); if (!settings.trustTarballsFromGitForges) // FIXME: computing the NAR hash here is wasteful if @@ -350,7 +350,7 @@ struct GitArchiveInputScheme : InputScheme std::optional getFingerprint(ref store, const Input & input) const override { if (auto rev = input.getRev()) - return rev->gitRev(); + return "github:" + rev->gitRev(); else return std::nullopt; } diff --git a/src/libfetchers/include/nix/fetchers/fetch-settings.hh b/src/libfetchers/include/nix/fetchers/fetch-settings.hh index 0d3f631a7f2..e2268203b56 100644 --- a/src/libfetchers/include/nix/fetchers/fetch-settings.hh +++ b/src/libfetchers/include/nix/fetchers/fetch-settings.hh @@ -123,6 +123,16 @@ struct Settings : public Config When empty, disables the global flake registry. )"}; + Setting nix219Compat{ + this, + false, + "nix-219-compat", + R"( + If enabled, Nix will generate lock files that are compatible with Nix 2.19. + In particular, Nix will use `git archive` rather than `libgit2` to copy Git inputs. + The resulting locks may not be compatible with Nix >= 2.20. + )"}; + ref getCache() const; ref getTarballCache() const; diff --git a/src/libfetchers/include/nix/fetchers/git-utils.hh b/src/libfetchers/include/nix/fetchers/git-utils.hh index 19b5f0f6b0f..6db6e09e8a0 100644 --- a/src/libfetchers/include/nix/fetchers/git-utils.hh +++ b/src/libfetchers/include/nix/fetchers/git-utils.hh @@ -22,6 +22,15 @@ struct GitFileSystemObjectSink : ExtendedFileSystemObjectSink virtual Hash flush() = 0; }; +struct GitAccessorOptions +{ + bool exportIgnore = false; + bool smudgeLfs = false; + bool submodules = false; // Currently implemented in GitInputScheme rather than GitAccessor + + std::string makeFingerprint(const Hash & rev) const; +}; + struct GitRepo { virtual ~GitRepo() {} @@ -89,10 +98,10 @@ struct GitRepo virtual bool hasObject(const Hash & oid) = 0; virtual ref - getAccessor(const Hash & rev, bool exportIgnore, std::string displayPrefix, bool smudgeLfs = false) = 0; + getAccessor(const Hash & rev, const GitAccessorOptions & options, std::string displayPrefix) = 0; - virtual ref - getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError) = 0; + virtual ref getAccessor( + const WorkdirInfo & wd, const GitAccessorOptions & options, MakeNotAllowedError makeNotAllowedError) = 0; virtual ref getFileSystemObjectSink() = 0; diff --git a/src/libfetchers/mercurial.cc b/src/libfetchers/mercurial.cc index c349e5dbf10..9f984bec6bb 100644 --- a/src/libfetchers/mercurial.cc +++ b/src/libfetchers/mercurial.cc @@ -345,7 +345,7 @@ struct MercurialInputScheme : InputScheme std::optional getFingerprint(ref store, const Input & input) const override { if (auto rev = input.getRev()) - return rev->gitRev(); + return "hg:" + rev->gitRev(); else return std::nullopt; } diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index e94b066bd20..fbdd1185070 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -136,7 +136,7 @@ static DownloadTarballResult downloadTarball_( .treeHash = treeHash, .lastModified = (time_t) getIntAttr(infoAttrs, "lastModified"), .immutableUrl = maybeGetStrAttr(infoAttrs, "immutableUrl"), - .accessor = settings.getTarballCache()->getAccessor(treeHash, false, displayPrefix), + .accessor = settings.getTarballCache()->getAccessor(treeHash, {}, displayPrefix), }; }; @@ -407,9 +407,9 @@ struct TarballInputScheme : CurlInputScheme std::optional getFingerprint(ref store, const Input & input) const override { if (auto narHash = input.getNarHash()) - return narHash->to_string(HashFormat::SRI, true); + return "tarball:" + narHash->to_string(HashFormat::SRI, true); else if (auto rev = input.getRev()) - return rev->gitRev(); + return "tarball:" + rev->gitRev(); else return std::nullopt; } diff --git a/src/libflake/flakeref.cc b/src/libflake/flakeref.cc index e9986425b82..3c0531f14dd 100644 --- a/src/libflake/flakeref.cc +++ b/src/libflake/flakeref.cc @@ -3,6 +3,7 @@ #include "nix/util/url.hh" #include "nix/util/url-parts.hh" #include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/fetch-settings.hh" namespace nix { @@ -61,7 +62,8 @@ static std::pair fromParsedURL(const fetchers::Settings & fetchSettings, ParsedURL && parsedURL, bool isFlake) { auto dir = getOr(parsedURL.query, "dir", ""); - parsedURL.query.erase("dir"); + if (!fetchSettings.nix219Compat) + parsedURL.query.erase("dir"); std::string fragment; std::swap(fragment, parsedURL.fragment); diff --git a/tests/functional/fetchGit.sh b/tests/functional/fetchGit.sh index be8b5cb34af..f4a03cce3c9 100755 --- a/tests/functional/fetchGit.sh +++ b/tests/functional/fetchGit.sh @@ -310,3 +310,59 @@ git -C "$empty" config user.name "Foobar" git -C "$empty" commit --allow-empty --allow-empty-message --message "" nix eval --impure --expr "let attrs = builtins.fetchGit $empty; in assert attrs.lastModified != 0; assert attrs.rev != \"0000000000000000000000000000000000000000\"; assert attrs.revCount == 1; true" + +# Test backward compatibility hack for Nix < 2.20 locks / fetchTree calls that expect Git filters to be applied. +eol="$TEST_ROOT/git-eol" +mkdir -p "$eol" +git init "$eol" +git -C "$eol" config user.email "foobar@example.com" +git -C "$eol" config user.name "Foobar" +printf "Hello\nWorld\n" > "$eol/crlf" +printf "ignore me" > "$eol/ignored" +git -C "$eol" add crlf ignored +git -C "$eol" commit -a -m Initial +echo "Version: \$Format:%s\$" > "$eol/version" +printf "crlf text eol=crlf\nignored export-ignore\nversion export-subst\n" > "$eol/.gitattributes" +git -C "$eol" add .gitattributes version +git -C "$eol" commit -a -m 'Apply gitattributes' + +rev="$(git -C "$eol" rev-parse HEAD)" + +export _NIX_TEST_BARF_ON_UNCACHEABLE=1 + +oldHash="sha256-cOuYSqDjvOBmKCuH5nXEfHRIAUVJZlictW0raF+3ynk=" +newHash="sha256-WZ5VePvmUcbRbkWLlNtCywWrAcr7EvVeJP8xKdZR7pc=" + +expectStderr 0 nix eval --expr \ + "let tree = builtins.fetchTree { type = \"git\"; url = \"file://$eol\"; rev = \"$rev\"; narHash = \"$oldHash\"; }; in assert builtins.readFile \"\${tree}/crlf\" == \"Hello\r\nWorld\r\n\"; assert !builtins.pathExists \"\${tree}/ignored\"; assert builtins.readFile \"\${tree}/version\" == \"Version: Apply gitattributes\n\"; true" \ + | grepQuiet "Please update the NAR hash to '$newHash'" + +nix eval --expr \ + "let tree = builtins.fetchTree { type = \"git\"; url = \"file://$eol\"; rev = \"$rev\"; narHash = \"$newHash\"; }; in assert builtins.readFile \"\${tree}/crlf\" == \"Hello\nWorld\n\"; assert builtins.pathExists \"\${tree}/ignored\"; assert builtins.readFile \"\${tree}/version\" == \"Version: \$Format:%s\$\n\"; true" + +expectStderr 102 nix eval --expr \ + "builtins.fetchTree { type = \"git\"; url = \"file://$eol\"; rev = \"$rev\"; narHash = \"sha256-DLDvcwdcwCxnuPTxSQ6gLAyopB20lD0bOQoQB3i2hsA=\"; }" \ + | grepQuiet "NAR hash mismatch" + +mkdir -p "$TEST_ROOT"/flake +cat > "$TEST_ROOT"/flake/flake.nix << EOF +{ + inputs.eol = { type = "git"; url = "file://$eol"; rev = "$rev"; flake = false; }; + outputs = { self, eol }: rec { + crlf = builtins.readFile "\${eol}/crlf"; + isLegacy = assert crlf == "Hello\r\nWorld\r\n"; true; + isModern = assert crlf == "Hello\nWorld\n"; true; + }; +} +EOF + +# Test locking with Nix < 2.20 semantics (i.e. using `git archive`). +nix eval --nix-219-compat "path:$TEST_ROOT/flake"#isLegacy +nix eval "path:$TEST_ROOT/flake"#isLegacy +[[ $(jq -r .nodes.eol.locked.narHash < "$TEST_ROOT"/flake/flake.lock) = "$oldHash" ]] + +# Test locking with Nix >= 2.20 semantics (i.e. using libgit2). +rm "$TEST_ROOT"/flake/flake.lock +nix eval "path:$TEST_ROOT/flake"#isModern +nix eval --nix-219-compat "path:$TEST_ROOT/flake"#isModern +[[ $(jq -r .nodes.eol.locked.narHash < "$TEST_ROOT"/flake/flake.lock) = "$newHash" ]]