Skip to content

Commit 6e632cd

Browse files
committed
Backward compatibility hack for Git inputs depending on Nix < 2.20 behaviour
Before Nix 2.20, we used git, which applies Git filters (in particular doing end-of-line conversion based on .gitattributes), `export-ignore` and `export-subst`. In 2.20, we switched to libgit2 and stopped applying those, which is probably better for reproducibility (e.g. `export-subst` can do pretty crazy things). However, that breaks existing lock files / fetchTree calls for Git inputs that rely on them, since it invalidates the NAR hash. So as a backward compatibility hack, we now check the NAR hash computed over the Git tree without them. If there is a hash mismatch, we try again *with* them (by calling `git archive`, just like Nix < 2.20). If that succeeds, we print a warning and return the filtered tree. This is inefficient, but should be fine for legacy flakes.
1 parent 3d33dfc commit 6e632cd

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

src/libfetchers/git.cc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "nix/util/json-utils.hh"
1717
#include "nix/util/archive.hh"
1818
#include "nix/util/mounted-source-accessor.hh"
19+
#include "nix/fetchers/fetch-to-store.hh"
1920

2021
#include <regex>
2122
#include <string.h>
@@ -643,6 +644,30 @@ struct GitInputScheme : InputScheme
643644
+ (getLfsAttr(input) ? ";l" : "");
644645
}
645646

647+
/**
648+
* Get a `SourceAccessor` for the given Git revision by creating a git archive and unpacking it to the Nix store.
649+
* This is used for Nix < 2.20 compatibility.
650+
*/
651+
ref<SourceAccessor> getGitArchiveAccessor(
652+
Store & store, RepoInfo & repoInfo, const std::filesystem::path & repoDir, const Hash & rev) const
653+
{
654+
auto tmpDir = createTempDir();
655+
AutoDelete delTmpDir(tmpDir, true);
656+
657+
auto source = sinkToSource([&](Sink & sink) {
658+
runProgram2(
659+
{.program = "git",
660+
.args = {"-C", repoDir, "--git-dir", repoInfo.gitDir, "archive", rev.gitRev()},
661+
.standardOut = &sink});
662+
});
663+
664+
unpackTarfile(*source, tmpDir);
665+
666+
auto storePath = store.addToStore("source", {getFSSourceAccessor(), CanonPath(tmpDir)});
667+
668+
return ref{store.getFSAccessor(storePath)};
669+
}
670+
646671
std::pair<ref<SourceAccessor>, Input>
647672
getAccessorFromCommit(const Settings & settings, ref<Store> store, RepoInfo & repoInfo, Input && input) const
648673
{
@@ -791,6 +816,34 @@ struct GitInputScheme : InputScheme
791816
};
792817
auto accessor = repo->getAccessor(rev, options, "«" + input.to_string(true) + "»");
793818

819+
/* Backward compatibility hack for locks produced by Nix < 2.20 that depend on Nix applying Git filters,
820+
* `export-ignore` or `export-subst`. Nix >= 2.20 doesn't do those, so we may get a NAR hash mismatch. If that
821+
* happens, try again using `git archive`. */
822+
if (auto expectedNarHash = input.getNarHash()) {
823+
if (accessor->pathExists(CanonPath(".gitattributes"))) {
824+
auto narHashNew =
825+
fetchToStore2(settings, *store, {accessor}, FetchMode::DryRun, input.getName()).second;
826+
if (expectedNarHash != narHashNew) {
827+
GitAccessorOptions options2{.exportIgnore = true};
828+
auto accessor2 = getGitArchiveAccessor(*store, repoInfo, repoDir, rev);
829+
accessor2->fingerprint = options2.makeFingerprint(rev) + ";f";
830+
auto narHashOld =
831+
fetchToStore2(settings, *store, {accessor2}, FetchMode::DryRun, input.getName()).second;
832+
if (expectedNarHash == narHashOld) {
833+
warn(
834+
"Git input '%s' specifies a NAR hash '%s' that was created by Nix < 2.20.\n"
835+
"Nix >= 2.20 does not apply Git filters, `export-ignore` and `export-subst` by default, which changes the NAR hash.\n"
836+
"Please update the NAR hash to '%s'.",
837+
input.to_string(),
838+
expectedNarHash->to_string(HashFormat::SRI, true),
839+
narHashNew.to_string(HashFormat::SRI, true));
840+
accessor = accessor2;
841+
options = options2;
842+
}
843+
}
844+
}
845+
}
846+
794847
/* If the repo has submodules, fetch them and return a mounted
795848
input accessor consisting of the accessor for the top-level
796849
repo and the accessors for the submodules. */

tests/functional/fetchGit.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,36 @@ git -C "$empty" config user.name "Foobar"
310310
git -C "$empty" commit --allow-empty --allow-empty-message --message ""
311311

312312
nix eval --impure --expr "let attrs = builtins.fetchGit $empty; in assert attrs.lastModified != 0; assert attrs.rev != \"0000000000000000000000000000000000000000\"; assert attrs.revCount == 1; true"
313+
314+
# Test backward compatibility hack for Nix < 2.20 locks / fetchTree calls that expect Git filters to be applied.
315+
eol="$TEST_ROOT/git-eol"
316+
mkdir -p "$eol"
317+
git init "$eol"
318+
git -C "$eol" config user.email "foobar@example.com"
319+
git -C "$eol" config user.name "Foobar"
320+
printf "Hello\nWorld\n" > "$eol/crlf"
321+
printf "ignore me" > "$eol/ignored"
322+
git -C "$eol" add crlf ignored
323+
git -C "$eol" commit -a -m Initial
324+
echo "Version: \$Format:%s\$" > "$eol/version"
325+
printf "crlf text eol=crlf\nignored export-ignore\nversion export-subst\n" > "$eol/.gitattributes"
326+
git -C "$eol" add .gitattributes version
327+
git -C "$eol" commit -a -m 'Apply gitattributes'
328+
329+
rev="$(git -C "$eol" rev-parse HEAD)"
330+
331+
export _NIX_TEST_BARF_ON_UNCACHEABLE=1
332+
333+
oldHash="sha256-cOuYSqDjvOBmKCuH5nXEfHRIAUVJZlictW0raF+3ynk="
334+
newHash="sha256-WZ5VePvmUcbRbkWLlNtCywWrAcr7EvVeJP8xKdZR7pc="
335+
336+
expectStderr 0 nix eval --expr \
337+
"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" \
338+
| grepQuiet "Please update the NAR hash to '$newHash'"
339+
340+
nix eval --expr \
341+
"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"
342+
343+
expectStderr 102 nix eval --expr \
344+
"builtins.fetchTree { type = \"git\"; url = \"file://$eol\"; rev = \"$rev\"; narHash = \"sha256-DLDvcwdcwCxnuPTxSQ6gLAyopB20lD0bOQoQB3i2hsA=\"; }" \
345+
| grepQuiet "NAR hash mismatch"

0 commit comments

Comments
 (0)